mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +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
|
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
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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 };
|
||||||
|
@ -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);
|
||||||
|
@ -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} />
|
||||||
|
@ -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;
|
||||||
|
@ -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 };
|
||||||
|
34
frontend/types/custom.d.ts
vendored
34
frontend/types/custom.d.ts
vendored
@ -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 {};
|
||||||
|
@ -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
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.
|
// 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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
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"
|
"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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user