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 data json NOT NULL
); );
CREATE TABLE db_window (
windowid varchar(36) PRIMARY KEY,
data json NOT NULL
);
CREATE TABLE db_workspace ( CREATE TABLE db_workspace (
workspaceid varchar(36) PRIMARY KEY, workspaceid varchar(36) PRIMARY KEY,
data json NOT NULL data json NOT NULL

View File

@ -19,6 +19,16 @@ const App = () => {
}; };
const AppInner = () => { 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 ( return (
<div className="mainapp"> <div className="mainapp">
<div className="titlebar"></div> <div className="titlebar"></div>

View File

@ -31,6 +31,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => {
setDims({ width: newWidth, height: newHeight }); setDims({ width: newWidth, height: newHeight });
} }
}, [blockRef.current]); }, [blockRef.current]);
let blockElem: JSX.Element = null; let blockElem: JSX.Element = null;
const blockAtom = blockDataMap.get(blockId); const blockAtom = blockDataMap.get(blockId);
const blockData = jotai.useAtomValue(blockAtom); const blockData = jotai.useAtomValue(blockAtom);

View File

@ -3,6 +3,10 @@
import "./quickelems.less"; import "./quickelems.less";
function CenteredLoadingDiv() {
return <CenteredDiv>loading...</CenteredDiv>;
}
function CenteredDiv({ children }: { children: React.ReactNode }) { function CenteredDiv({ children }: { children: React.ReactNode }) {
return ( return (
<div className="centered-div"> <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 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 blockDataMap = new Map<string, jotai.Atom<wstore.Block>>();
const blockAtomCache = new Map<string, Map<string, jotai.Atom<any>>>(); const blockAtomCache = new Map<string, Map<string, jotai.Atom<any>>>();
const atoms = { const atoms = {
activeTabId: jotai.atom<string>(tabId1), activeTabId: jotai.atom<string>(tabId1),
tabsAtom: jotai.atom<TabData[]>(tabArr), tabsAtom: jotai.atom<wstore.Tab[]>(tabArr),
blockDataMap: blockDataMap, 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 }; 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); let tabArr = globalStore.get(atoms.tabsAtom);
const newTabArr = produce(tabArr, (draft) => { const newTabArr = produce(tabArr, (draft) => {
const tab = draft.find((tab) => tab.tabid == tabId); const tab = draft.find((tab) => tab.tabid == tabId);
tab.blockIds.push(blockId); tab.blockids.push(blockId);
}); });
globalStore.set(atoms.tabsAtom, newTabArr); globalStore.set(atoms.tabsAtom, newTabArr);
} }
@ -93,7 +98,7 @@ function removeBlockFromTab(tabId: string, blockId: string) {
let tabArr = globalStore.get(atoms.tabsAtom); let tabArr = globalStore.get(atoms.tabsAtom);
const newTabArr = produce(tabArr, (draft) => { const newTabArr = produce(tabArr, (draft) => {
const tab = draft.find((tab) => tab.tabid == tabId); 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); globalStore.set(atoms.tabsAtom, newTabArr);
removeBlock(blockId); removeBlock(blockId);

View File

@ -16,7 +16,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
} }
return ( return (
<div className="tabcontent"> <div className="tabcontent">
{tabData.blockIds.map((blockId: string) => { {tabData.blockids.map((blockId: string) => {
return ( return (
<div key={blockId} className="block-container"> <div key={blockId} className="block-container">
<Block tabId={tabId} blockId={blockId} /> <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 * as util from "@/util/util";
import { CenteredDiv } from "../element/quickelems"; import { CenteredDiv } from "../element/quickelems";
import { DirectoryTable } from "@/element/directorytable"; import { DirectoryTable } from "@/element/directorytable";
import * as wstore from "@/gopkg/wstore";
import "./view.less"; import "./view.less";
@ -61,7 +62,7 @@ function DirectoryPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<str
} }
function PreviewView({ blockId }: { blockId: string }) { 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", () => const fileNameAtom = useBlockAtom(blockId, "preview:filename", () =>
jotai.atom<string>((get) => { jotai.atom<string>((get) => {
return get(blockDataAtom)?.meta?.file; return get(blockDataAtom)?.meta?.file;

View File

@ -8,11 +8,15 @@ import { clsx } from "clsx";
import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global"; import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { BlockService } from "@/bindings/blockservice"; import { BlockService } from "@/bindings/blockservice";
import { ClientService } from "@/bindings/clientservice";
import { Workspace } from "@/gopkg/wstore";
import * as wstore from "@/gopkg/wstore"; import * as wstore from "@/gopkg/wstore";
import * as jotaiUtil from "jotai/utils";
import "./workspace.less"; 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); const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId);
return ( return (
<div className={clsx("tab", { active: activeTab === tab.tabid })} onClick={() => setActiveTab(tab.tabid)}> <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 [tabData, setTabData] = jotai.useAtom(atoms.tabsAtom);
const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId);
const tabs = jotai.useAtomValue(atoms.tabsAtom); const tabs = jotai.useAtomValue(atoms.tabsAtom);
const client = jotai.useAtomValue(atoms.clientAtom);
function handleAddTab() { function handleAddTab() {
const newTabId = uuidv4(); const newTabId = uuidv4();
const newTabName = "Tab " + (tabData.length + 1); const newTabName = "Tab " + (tabData.length + 1);
setTabData([...tabData, { name: newTabName, tabid: newTabId, blockIds: [] }]); setTabData([...tabData, { name: newTabName, tabid: newTabId, blockids: [] }]);
setActiveTab(newTabId); setActiveTab(newTabId);
} }
@ -48,8 +53,8 @@ function TabBar() {
function Widgets() { function Widgets() {
const activeTabId = jotai.useAtomValue(atoms.activeTabId); const activeTabId = jotai.useAtomValue(atoms.activeTabId);
async function createBlock(blockDef: BlockDef) { async function createBlock(blockDef: wstore.BlockDef) {
const rtOpts = { termsize: { rows: 25, cols: 80 } }; const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } });
const rtnBlock: wstore.Block = await BlockService.CreateBlock(blockDef, rtOpts); const rtnBlock: wstore.Block = await BlockService.CreateBlock(blockDef, rtOpts);
const newBlockAtom = jotai.atom(rtnBlock); const newBlockAtom = jotai.atom(rtnBlock);
blockDataMap.set(rtnBlock.blockid, newBlockAtom); blockDataMap.set(rtnBlock.blockid, newBlockAtom);
@ -57,25 +62,25 @@ function Widgets() {
} }
async function clickTerminal() { async function clickTerminal() {
const termBlockDef = { const termBlockDef = new wstore.BlockDef({
controller: "shell", controller: "shell",
view: "term", view: "term",
}; });
createBlock(termBlockDef); createBlock(termBlockDef);
} }
async function clickPreview(fileName: string) { async function clickPreview(fileName: string) {
const markdownDef = { const markdownDef = new wstore.BlockDef({
view: "preview", view: "preview",
meta: { file: fileName }, meta: { file: fileName },
}; });
createBlock(markdownDef); createBlock(markdownDef);
} }
async function clickPlot() { async function clickPlot() {
const plotDef = { const plotDef = new wstore.BlockDef({
view: "plot", view: "plot",
}; });
createBlock(plotDef); 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 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 ( return (
<div className="workspace"> <div className="workspace">
<TabBar /> <TabBar />
<div className="workspace-tabcontent"> <div className="workspace-tabcontent">
<TabContent key={activeTabId} tabId={activeTabId} /> <TabContent key={workspaceId} tabId={activeTabId} />
<Widgets /> <Widgets />
</div> </div>
</div> </div>
); );
} }
export { Workspace }; export { WorkspaceElem as Workspace };

View File

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

View File

@ -5,10 +5,35 @@ import * as React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { App } from "./app/app"; import { App } from "./app/app";
import { loadFonts } from "./util/fontutil"; 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(); 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 reactElem = React.createElement(App, null, null);
let elem = document.getElementById("main"); let elem = document.getElementById("main");
let root = createRoot(elem); 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. // Note, main.go needs to be in the root of the project for the go:embed directive to work.
import ( import (
"context"
"embed" "embed"
"log" "log"
"net/http" "net/http"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/wavetermdev/thenextwave/pkg/blockstore" "github.com/wavetermdev/thenextwave/pkg/blockstore"
"github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/service/blockservice" "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/fileservice"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
@ -33,10 +36,10 @@ func createAppMenu(app *application.App) *application.Menu {
menu := application.NewMenu() menu := application.NewMenu()
menu.AddRole(application.AppMenu) menu.AddRole(application.AppMenu)
fileMenu := menu.AddSubmenu("File") fileMenu := menu.AddSubmenu("File")
newWindow := fileMenu.Add("New Window") // newWindow := fileMenu.Add("New Window")
newWindow.OnClick(func(appContext *application.Context) { // newWindow.OnClick(func(appContext *application.Context) {
createWindow(app) // createWindow(app)
}) // })
closeWindow := fileMenu.Add("Close Window") closeWindow := fileMenu.Add("Close Window")
closeWindow.OnClick(func(appContext *application.Context) { closeWindow.OnClick(func(appContext *application.Context) {
app.CurrentWindow().Close() app.CurrentWindow().Close()
@ -48,7 +51,7 @@ func createAppMenu(app *application.App) *application.Menu {
return menu return menu
} }
func createWindow(app *application.App) { func createWindow(windowData *wstore.Window, app *application.App) {
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Wave Terminal", Title: "Wave Terminal",
Mac: application.MacWindow{ Mac: application.MacWindow{
@ -56,13 +59,18 @@ func createWindow(app *application.App) {
Backdrop: application.MacBackdropTranslucent, Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset, TitleBar: application.MacTitleBarHiddenInset,
}, },
BackgroundColour: application.NewRGB(27, 38, 54), BackgroundColour: application.NewRGB(0, 0, 0),
URL: "/public/index.html", 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) eventbus.RegisterWailsWindow(window)
window.On(events.Common.WindowClosing, func(event *application.WindowEvent) { window.On(events.Common.WindowClosing, func(event *application.WindowEvent) {
eventbus.UnregisterWailsWindow(window.ID()) eventbus.UnregisterWailsWindow(window.ID())
}) })
window.Show()
} }
type waveAssetHandler struct { type waveAssetHandler struct {
@ -110,6 +118,11 @@ func main() {
log.Printf("error initializing wstore: %v\n", err) log.Printf("error initializing wstore: %v\n", err)
return return
} }
err = wstore.EnsureInitialData()
if err != nil {
log.Printf("error ensuring initial data: %v\n", err)
return
}
app := application.New(application.Options{ app := application.New(application.Options{
Name: "NextWave", Name: "NextWave",
@ -117,6 +130,7 @@ func main() {
Services: []application.Service{ Services: []application.Service{
application.NewService(&fileservice.FileService{}), application.NewService(&fileservice.FileService{}),
application.NewService(&blockservice.BlockService{}), application.NewService(&blockservice.BlockService{}),
application.NewService(&clientservice.ClientService{}),
}, },
Icon: appIcon, Icon: appIcon,
Assets: application.AssetOptions{ Assets: application.AssetOptions{
@ -130,7 +144,23 @@ func main() {
app.SetMenu(menu) app.SetMenu(menu)
eventbus.RegisterWailsApp(app) 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() eventbus.Start()
defer eventbus.Shutdown() defer eventbus.Shutdown()

View File

@ -4,12 +4,14 @@
package blockcontroller package blockcontroller
import ( import (
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"sync" "sync"
"time"
"github.com/creack/pty" "github.com/creack/pty"
"github.com/google/uuid" "github.com/google/uuid"
@ -24,6 +26,8 @@ const (
BlockController_Cmd = "cmd" BlockController_Cmd = "cmd"
) )
const DefaultTimeout = 2 * time.Second
var globalLock = &sync.Mutex{} var globalLock = &sync.Mutex{}
var blockControllerMap = make(map[string]*BlockController) var blockControllerMap = make(map[string]*BlockController)
@ -32,11 +36,18 @@ type BlockController struct {
BlockId string BlockId string
BlockDef *wstore.BlockDef BlockDef *wstore.BlockDef
InputCh chan BlockCommand InputCh chan BlockCommand
Status string
ShellProc *shellexec.ShellProc ShellProc *shellexec.ShellProc
ShellInputCh chan *InputCommand 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) { func jsonDeepCopy(val map[string]any) (map[string]any, error) {
barr, err := json.Marshal(val) barr, err := json.Marshal(val)
if err != nil { if err != nil {
@ -50,10 +61,9 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) {
return rtn, nil 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() blockId := uuid.New().String()
blockData := &wstore.Block{ blockData := &wstore.Block{
Lock: &sync.Mutex{},
BlockId: blockId, BlockId: blockId,
BlockDef: bdef, BlockDef: bdef,
Controller: bdef.Controller, Controller: bdef.Controller,
@ -65,7 +75,10 @@ func CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Blo
if err != nil { if err != nil {
return nil, fmt.Errorf("error copying meta: %w", err) 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 != "" { if blockData.Controller != "" {
StartBlockController(blockId, blockData) StartBlockController(blockId, blockData)
} }
@ -179,10 +192,10 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
func (bc *BlockController) Run(bdata *wstore.Block) { func (bc *BlockController) Run(bdata *wstore.Block) {
defer func() { defer func() {
bdata.WithLock(func() { bc.WithLock(func() {
// if the controller had an error status, don't change it // if the controller had an error status, don't change it
if bdata.ControllerStatus == "running" { if bc.Status == "running" {
bdata.ControllerStatus = "done" bc.Status = "done"
} }
}) })
eventbus.SendEvent(application.WailsEvent{ eventbus.SendEvent(application.WailsEvent{
@ -193,8 +206,8 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
defer globalLock.Unlock() defer globalLock.Unlock()
delete(blockControllerMap, bc.BlockId) delete(blockControllerMap, bc.BlockId)
}() }()
bdata.WithLock(func() { bc.WithLock(func() {
bdata.ControllerStatus = "running" bc.Status = "running"
}) })
// only controller is "shell" for now // only controller is "shell" for now
@ -221,9 +234,6 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
func StartBlockController(blockId string, bdata *wstore.Block) { func StartBlockController(blockId string, bdata *wstore.Block) {
if bdata.Controller != BlockController_Shell { if bdata.Controller != BlockController_Shell {
log.Printf("unknown controller %q\n", bdata.Controller) log.Printf("unknown controller %q\n", bdata.Controller)
bdata.WithLock(func() {
bdata.ControllerStatus = "error"
})
return return
} }
globalLock.Lock() globalLock.Lock()
@ -234,6 +244,7 @@ func StartBlockController(blockId string, bdata *wstore.Block) {
bc := &BlockController{ bc := &BlockController{
Lock: &sync.Mutex{}, Lock: &sync.Mutex{},
BlockId: blockId, BlockId: blockId,
Status: "init",
InputCh: make(chan BlockCommand), InputCh: make(chan BlockCommand),
} }
blockControllerMap[blockId] = bc blockControllerMap[blockId] = bc
@ -246,23 +257,34 @@ func GetBlockController(blockId string) *BlockController {
return blockControllerMap[blockId] 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) { switch cmd := cmdGen.(type) {
case *MessageCommand: case *MessageCommand:
log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message) log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message)
return nil
case *SetViewCommand: case *SetViewCommand:
log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View) log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View)
block := wstore.BlockMap.Get(blockId) block, err := wstore.DBGet[wstore.Block](ctx, blockId)
if block != nil { if err != nil {
block.WithLock(func() { return fmt.Errorf("error getting block: %w", err)
block.View = cmd.View
})
} }
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: case *SetMetaCommand:
log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta)
block := wstore.BlockMap.Get(blockId) block, err := wstore.DBGet[wstore.Block](ctx, blockId)
if block != nil { if err != nil {
block.WithLock(func() { return fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return nil
}
for k, v := range cmd.Meta { for k, v := range cmd.Meta {
if v == nil { if v == nil {
delete(block.Meta, k) delete(block.Meta, k)
@ -270,7 +292,12 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) {
} }
block.Meta[k] = v 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" "path"
"time" "time"
"github.com/wavetermdev/thenextwave/pkg/util/migrateutil"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "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/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/sawka/txwrap" "github.com/sawka/txwrap"
@ -40,7 +38,7 @@ func InitBlockstore() error {
if err != nil { if err != nil {
return err return err
} }
err = MigrateBlockstore() err = migrateutil.Migrate("blockstore", globalDB.DB, dbfs.BlockstoreMigrationFS, "migrations-blockstore")
if err != nil { if err != nil {
return err 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) { func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) {
return txwrap.WithTxRtn(ctx, globalDB, fn) 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" "time"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
type BlockService struct{} type BlockService struct{}
func (bs *BlockService) CreateBlock(bdefMap map[string]any, rtOptsMap map[string]any) (*wstore.Block, error) { const DefaultTimeout = 2 * time.Second
var bdef wstore.BlockDef
err := utilfn.JsonMapToStruct(bdefMap, &bdef) func (bs *BlockService) CreateBlock(bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) {
if err != nil { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
return nil, fmt.Errorf("error unmarshalling BlockDef: %w", err) defer cancelFn()
if bdef == nil {
return nil, fmt.Errorf("block definition is nil")
} }
var rtOpts wstore.RuntimeOpts if rtOpts == nil {
err = utilfn.JsonMapToStruct(rtOptsMap, &rtOpts) return nil, fmt.Errorf("runtime options is nil")
if err != nil {
return nil, fmt.Errorf("error unmarshalling RuntimeOpts: %w", err)
} }
blockData, err := blockcontroller.CreateBlock(&bdef, &rtOpts) blockData, err := blockcontroller.CreateBlock(ctx, bdef, rtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) 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) { func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn() defer cancelFn()
blockData, err := wstore.BlockGet(ctx, blockId) blockData, err := wstore.DBGet[wstore.Block](ctx, blockId)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting block data: %w", err) 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" "context"
"fmt" "fmt"
"sync" "sync"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
@ -18,7 +19,28 @@ var TabMap = ds.NewSyncMap[*Tab]()
var BlockMap = ds.NewSyncMap[*Block]() var BlockMap = ds.NewSyncMap[*Block]()
type Client struct { 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 { type Workspace struct {
@ -28,6 +50,10 @@ type Workspace struct {
TabIds []string `json:"tabids"` TabIds []string `json:"tabids"`
} }
func (ws Workspace) GetId() string {
return ws.WorkspaceId
}
func (ws *Workspace) WithLock(f func()) { func (ws *Workspace) WithLock(f func()) {
ws.Lock.Lock() ws.Lock.Lock()
defer ws.Lock.Unlock() defer ws.Lock.Unlock()
@ -41,6 +67,10 @@ type Tab struct {
BlockIds []string `json:"blockids"` BlockIds []string `json:"blockids"`
} }
func (tab Tab) GetId() string {
return tab.TabId
}
func (tab *Tab) WithLock(f func()) { func (tab *Tab) WithLock(f func()) {
tab.Lock.Lock() tab.Lock.Lock()
defer tab.Lock.Unlock() defer tab.Lock.Unlock()
@ -67,25 +97,31 @@ type RuntimeOpts struct {
WinSize WinSize `json:"winsize,omitempty"` WinSize WinSize `json:"winsize,omitempty"`
} }
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
type WinSize struct { type WinSize struct {
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
} }
type Block struct { type Block struct {
Lock *sync.Mutex `json:"-"`
BlockId string `json:"blockid"` BlockId string `json:"blockid"`
BlockDef *BlockDef `json:"blockdef"` BlockDef *BlockDef `json:"blockdef"`
Controller string `json:"controller"` Controller string `json:"controller"`
ControllerStatus string `json:"controllerstatus"`
View string `json:"view"` View string `json:"view"`
Meta map[string]any `json:"meta,omitempty"` Meta map[string]any `json:"meta,omitempty"`
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"` RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
} }
func (b Block) GetId() string {
return b.BlockId
}
// TODO remove
func (b *Block) WithLock(f func()) { func (b *Block) WithLock(f func()) {
b.Lock.Lock()
defer b.Lock.Unlock()
f() f()
} }
@ -121,30 +157,60 @@ func CreateWorkspace() (*Workspace, error) {
return ws, nil return ws, nil
} }
func EnsureWorkspace(ctx context.Context) error { func EnsureInitialData() error {
wsCount, err := WorkspaceCount(ctx) ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
clientCount, err := DBGetCount[Client](ctx)
if err != nil { 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 return nil
} }
ws := &Workspace{ windowId := uuid.New().String()
Lock: &sync.Mutex{}, workspaceId := uuid.New().String()
WorkspaceId: uuid.New().String(), tabId := uuid.New().String()
Name: "default", 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 { if err != nil {
return fmt.Errorf("error inserting workspace: %w", err) return fmt.Errorf("error inserting workspace: %w", err)
} }
tab := &Tab{ tab := &Tab{
Lock: &sync.Mutex{},
TabId: uuid.New().String(), TabId: uuid.New().String(),
Name: "Tab 1", Name: "Tab 1",
BlockIds: []string{}, BlockIds: []string{},
} }
err = TabInsert(ctx, tab, ws.WorkspaceId) err = DBInsert(ctx, tab)
if err != nil { if err != nil {
return fmt.Errorf("error inserting tab: %w", err) return fmt.Errorf("error inserting tab: %w", err)
} }

View File

@ -6,92 +6,155 @@ package wstore
import ( import (
"context" "context"
"fmt" "fmt"
"reflect"
"github.com/google/uuid"
) )
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) { 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 return tx.GetInt(query), nil
}) })
} }
func WorkspaceInsert(ctx context.Context, ws *Workspace) error { func DBGetSingleton[T ObjectWithId](ctx context.Context) (*T, error) {
if ws.WorkspaceId == "" { return WithTxRtn(ctx, func(tx *TxWrap) (*T, error) {
ws.WorkspaceId = uuid.New().String() 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
}
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)
}
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 DBDelete[T ObjectWithId](ctx context.Context, id string) error {
return WithTx(ctx, func(tx *TxWrap) error { return WithTx(ctx, func(tx *TxWrap) error {
query := "INSERT INTO workspace (workspaceid, data) VALUES (?, ?)" var rtn T
tx.Exec(query, ws.WorkspaceId, TxJson(tx, ws)) 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 return nil
}) })
} }
func WorkspaceGet(ctx context.Context, workspaceId string) (*Workspace, error) { func DBUpdate[T ObjectWithId](ctx context.Context, val *T) error {
return WithTxRtn(ctx, func(tx *TxWrap) (*Workspace, error) { if val == nil {
query := "SELECT data FROM workspace WHERE workspaceid = ?" return fmt.Errorf("cannot update nil value")
jsonData := tx.GetString(query, workspaceId) }
return TxReadJson[Workspace](tx, jsonData), nil if (*val).GetId() == "" {
}) return fmt.Errorf("cannot update %T value with empty id", val)
} }
func WorkspaceUpdate(ctx context.Context, ws *Workspace) error {
return WithTx(ctx, func(tx *TxWrap) error { return WithTx(ctx, func(tx *TxWrap) error {
query := "UPDATE workspace SET data = ? WHERE workspaceid = ?" table := typeToTable[reflect.TypeOf(*val)]
tx.Exec(query, TxJson(tx, ws), ws.WorkspaceId) 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 return nil
}) })
} }
func addTabToWorkspace(ctx context.Context, workspaceId string, tabId string) error { func DBInsert[T ObjectWithId](ctx context.Context, val *T) error {
return WithTx(ctx, func(tx *TxWrap) error { if val == nil {
ws, err := WorkspaceGet(tx.Context(), workspaceId) return fmt.Errorf("cannot insert nil value")
if err != nil {
return err
} }
if ws == nil { if (*val).GetId() == "" {
return fmt.Errorf("workspace not found: %s", workspaceId) return fmt.Errorf("cannot insert %T value with empty id", val)
}
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 { return WithTx(ctx, func(tx *TxWrap) error {
query := "INSERT INTO tab (tabid, data) VALUES (?, ?)" table := typeToTable[reflect.TypeOf(*val)]
tx.Exec(query, tab.TabId, TxJson(tx, tab)) if table == "" {
return addTabToWorkspace(tx.Context(), workspaceId, tab.TabId) 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))
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 return nil
}) })
} }

View File

@ -11,13 +11,11 @@ import (
"path" "path"
"time" "time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/sawka/txwrap" "github.com/sawka/txwrap"
"github.com/wavetermdev/thenextwave/pkg/util/migrateutil"
"github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wavebase"
sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
dbfs "github.com/wavetermdev/thenextwave/db" dbfs "github.com/wavetermdev/thenextwave/db"
) )
@ -35,7 +33,7 @@ func InitWStore() error {
if err != nil { if err != nil {
return err return err
} }
err = MigrateWStore() err = migrateutil.Migrate("wstore", globalDB.DB, dbfs.WStoreMigrationFS, "migrations-wstore")
if err != nil { if err != nil {
return err return err
} }
@ -58,41 +56,6 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) {
return rtn, nil 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 { func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error {
return txwrap.WithTx(ctx, globalDB, fn) return txwrap.WithTx(ctx, globalDB, fn)
} }