mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
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:
parent
8173bc3c61
commit
134ba3c34c
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
|
@ -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} />
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
34
frontend/types/custom.d.ts
vendored
34
frontend/types/custom.d.ts
vendored
@ -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 {};
|
||||
|
@ -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
46
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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
56
pkg/service/clientservice/clientservice.go
Normal file
56
pkg/service/clientservice/clientservice.go
Normal 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
|
||||
}
|
67
pkg/util/migrateutil/migrateutil.go
Normal file
67
pkg/util/migrateutil/migrateutil.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user