diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 4a56d1fab..c50143039 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -3,8 +3,7 @@ import * as React from "react"; import * as jotai from "jotai"; -import { atoms, blockDataMap } from "@/store/global"; - +import * as WOS from "@/store/wos"; import { TerminalView } from "@/app/view/term"; import { PreviewView } from "@/app/view/preview"; import { PlotView } from "@/app/view/plotview"; @@ -33,9 +32,10 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => { }, [blockRef.current]); let blockElem: JSX.Element = null; - const blockAtom = blockDataMap.get(blockId); - const blockData = jotai.useAtomValue(blockAtom); - if (blockData.view === "term") { + const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + if (blockDataLoading) { + blockElem = Loading...; + } else if (blockData.view === "term") { blockElem = ; } else if (blockData.view === "preview") { blockElem = ; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index cf2dc10d8..f6bc3af99 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -4,13 +4,9 @@ import * as jotai from "jotai"; import * as rxjs from "rxjs"; import { Events } from "@wailsio/runtime"; -import { produce } from "immer"; -import { BlockService } from "@/bindings/blockservice"; -import * as wstore from "@/gopkg/wstore"; import * as WOS from "./wos"; const globalStore = jotai.createStore(); -const blockDataMap = new Map>(); const urlParams = new URLSearchParams(window.location.search); const globalWindowId = urlParams.get("windowid"); const globalClientId = urlParams.get("clientid"); @@ -19,8 +15,10 @@ const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom; globalStore.set(windowIdAtom, globalWindowId); globalStore.set(clientIdAtom, globalClientId); const uiContextAtom = jotai.atom((get) => { + const windowData = get(windowDataAtom); const uiContext: UIContext = { windowid: get(atoms.windowId), + activetabid: windowData.activetabid, }; return uiContext; }) as jotai.Atom; @@ -54,7 +52,6 @@ const atoms = { client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, - blockDataMap: blockDataMap, }; type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; @@ -93,6 +90,8 @@ Events.On("block:ptydata", (event: any) => { subject.next(data); }); +const blockAtomCache = new Map>>(); + function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom): jotai.Atom { let blockCache = blockAtomCache.get(blockId); if (blockCache == null) { @@ -103,8 +102,9 @@ function useBlockAtom(blockId: string, name: string, makeFn: () => jotai.Atom if (atom == null) { atom = makeFn(); blockCache.set(name, atom); + console.log("New BlockAtom", blockId, name); } return atom as jotai.Atom; } -export { globalStore, atoms, getBlockSubject, blockDataMap, useBlockAtom, WOS }; +export { globalStore, atoms, getBlockSubject, useBlockAtom, WOS }; diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index f9c625b4e..80c806247 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -94,7 +94,7 @@ function createWaveValueObject(oref: string, shouldFetch: boo } wov.pendingPromise = null; globalStore.set(wov.dataAtom, { value: val, loading: false }); - console.log("GetObject resolved", oref, Date.now() - startTs + "ms"); + console.log("WaveObj resolved", oref, Date.now() - startTs + "ms"); }); return wov; } @@ -113,6 +113,25 @@ function loadAndPinWaveObject(oref: string): Promise { return wov.pendingPromise; } +function useWaveObjectValueWithSuspense(oref: string): T { + let wov = waveObjectValueCache.get(oref); + if (wov == null) { + wov = createWaveValueObject(oref, true); + waveObjectValueCache.set(oref, wov); + } + React.useEffect(() => { + wov.refCount++; + return () => { + wov.refCount--; + }; + }, [oref]); + const dataValue = jotai.useAtomValue(wov.dataAtom); + if (dataValue.loading) { + throw wov.pendingPromise; + } + return dataValue.value; +} + function useWaveObjectValue(oref: string): [T, boolean] { let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -214,7 +233,6 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { ); prtn = prtn.then((val) => { if (val.updates) { - console.log(val.updates); updateWaveObjects(val.updates); } return val; @@ -222,14 +240,6 @@ function wrapObjectServiceCall(fnName: string, ...args: any[]): Promise { return prtn; } -function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { - return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); -} - -function SetActiveTab(tabId: string): Promise { - return wrapObjectServiceCall("SetActiveTab", tabId); -} - function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -239,10 +249,23 @@ function getStaticObjectValue(oref: string, getFn: jotai.Getter): T { return atomVal.value; } +function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> { + return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab); +} + +function SetActiveTab(tabId: string): Promise { + return wrapObjectServiceCall("SetActiveTab", tabId); +} + +function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> { + return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts); +} + export { makeORef, useWaveObject, useWaveObjectValue, + useWaveObjectValueWithSuspense, loadAndPinWaveObject, clearWaveObjectCache, updateWaveObject, @@ -251,4 +274,5 @@ export { getStaticObjectValue, AddTabToWorkspace, SetActiveTab, + CreateBlock, }; diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 97aa653f4..e237585b2 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -23,7 +23,7 @@ const TabContent = ({ tabId }: { tabId: string }) => { {tabData.blockids.map((blockId: string) => { return (
- +
); })} diff --git a/frontend/app/view/preview.tsx b/frontend/app/view/preview.tsx index e3ecaad88..dabd2bdbc 100644 --- a/frontend/app/view/preview.tsx +++ b/frontend/app/view/preview.tsx @@ -3,15 +3,16 @@ import * as React from "react"; import * as jotai from "jotai"; -import { atoms, blockDataMap, useBlockAtom } from "@/store/global"; +import { atoms, useBlockAtom } from "@/store/global"; import { Markdown } from "@/element/markdown"; 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 * as WOS from "@/store/wos"; import "./view.less"; +import { first } from "rxjs"; const MaxFileSize = 1024 * 1024 * 10; // 10MB @@ -62,10 +63,17 @@ function DirectoryPreview({ contentAtom }: { contentAtom: jotai.Atom = blockDataMap.get(blockId); + const blockData = WOS.useWaveObjectValueWithSuspense(WOS.makeORef("block", blockId)); + if (blockData == null) { + return ( +
+ Block Not Found +
+ ); + } const fileNameAtom = useBlockAtom(blockId, "preview:filename", () => jotai.atom((get) => { - return get(blockDataAtom)?.meta?.file; + return blockData?.meta?.file; }) ); const statFileAtom = useBlockAtom(blockId, "preview:statfile", () => diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 7c9866436..a31443b5a 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -5,13 +5,7 @@ import * as React from "react"; import * as jotai from "jotai"; import { TabContent } from "@/app/tab/tab"; import { clsx } from "clsx"; -import { atoms, 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 { atoms } from "@/store/global"; import * as WOS from "@/store/wos"; import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems"; @@ -36,7 +30,7 @@ function Tab({ tabId }: { tabId: string }) { ); } -function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: WaveWindow }) { +function TabBar({ workspace }: { workspace: Workspace }) { function handleAddTab() { const newTabName = `Tab-${workspace.tabids.length + 1}`; WOS.AddTabToWorkspace(newTabName, true); @@ -58,34 +52,31 @@ function Widgets() { const windowData = jotai.useAtomValue(atoms.waveWindow); const activeTabId = windowData.activetabid; - 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); - addBlockIdToTab(activeTabId, rtnBlock.blockid); + async function createBlock(blockDef: BlockDef) { + const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; + await WOS.CreateBlock(blockDef, rtOpts); } async function clickTerminal() { - const termBlockDef = new wstore.BlockDef({ + const termBlockDef = { controller: "shell", view: "term", - }); + }; createBlock(termBlockDef); } async function clickPreview(fileName: string) { - const markdownDef = new wstore.BlockDef({ + const markdownDef = { view: "preview", meta: { file: fileName }, - }); + }; createBlock(markdownDef); } async function clickPlot() { - const plotDef = new wstore.BlockDef({ + const plotDef: BlockDef = { view: "plot", - }); + }; createBlock(plotDef); } @@ -122,7 +113,7 @@ function WorkspaceElem() { const ws = jotai.useAtomValue(atoms.workspace); return (
- +
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 04cddaf96..e94ff3e02 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -4,6 +4,7 @@ declare global { type UIContext = { windowid: string; + activetabid: string; }; type ORef = { @@ -33,7 +34,7 @@ declare global { }; type BlockDef = { - controller: string; + controller?: string; view?: string; files?: { [key: string]: FileDef }; meta?: { [key: string]: any }; diff --git a/frontend/wave.ts b/frontend/wave.ts index c9d116ac6..98bdb412a 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -18,20 +18,12 @@ const urlParams = new URLSearchParams(window.location.search); const windowId = urlParams.get("windowid"); const clientId = urlParams.get("clientid"); -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(); +console.log("Wave Starting"); + document.addEventListener("DOMContentLoaded", async () => { + console.log("DOMContentLoaded"); // ensures client/window are loaded into the cache before rendering await WOS.loadAndPinWaveObject(WOS.makeORef("client", clientId)); const waveWindow = await WOS.loadAndPinWaveObject(WOS.makeORef("window", windowId)); @@ -40,6 +32,7 @@ document.addEventListener("DOMContentLoaded", async () => { let elem = document.getElementById("main"); let root = createRoot(elem); document.fonts.ready.then(() => { + console.log("Wave First Render"); root.render(reactElem); }); }); diff --git a/main.go b/main.go index 007106e0d..cb923c3a2 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func createWindow(windowData *wstore.Window, app *application.App) { Width: windowData.WinSize.Width, Height: windowData.WinSize.Height, }) - eventbus.RegisterWailsWindow(window) + eventbus.RegisterWailsWindow(window, windowData.OID) window.On(events.Common.WindowClosing, func(event *application.WindowEvent) { eventbus.UnregisterWailsWindow(window.ID()) }) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 70d03c7a8..d78f4edb9 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -14,7 +14,6 @@ import ( "time" "github.com/creack/pty" - "github.com/google/uuid" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/shellexec" @@ -61,41 +60,6 @@ func jsonDeepCopy(val map[string]any) (map[string]any, error) { return rtn, nil } -func CreateBlock(ctx context.Context, bdef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) { - // TODO - blockId := uuid.New().String() - blockData := &wstore.Block{ - OID: blockId, - BlockDef: bdef, - Controller: bdef.Controller, - View: bdef.View, - RuntimeOpts: rtOpts, - } - var err error - blockData.Meta, err = jsonDeepCopy(bdef.Meta) - if err != nil { - return nil, fmt.Errorf("error copying meta: %w", err) - } - err = wstore.DBInsert(ctx, blockData) - if err != nil { - return nil, fmt.Errorf("error inserting block: %w", err) - } - if blockData.Controller != "" { - StartBlockController(blockId, blockData) - } - return blockData, nil -} - -func CloseBlock(blockId string) { - // TODO - bc := GetBlockController(blockId) - if bc == nil { - return - } - bc.Close() - close(bc.InputCh) -} - func (bc *BlockController) setShellProc(shellProc *shellexec.ShellProc) error { bc.Lock.Lock() defer bc.Lock.Unlock() @@ -232,15 +196,23 @@ 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) - return +func StartBlockController(ctx context.Context, blockId string) error { + blockData, err := wstore.DBMustGet[*wstore.Block](ctx, blockId) + if err != nil { + return fmt.Errorf("error getting block: %w", err) + } + if blockData.Controller == "" { + // nothing to start + return nil + } + if blockData.Controller != BlockController_Shell { + return fmt.Errorf("unknown controller %q", blockData.Controller) } globalLock.Lock() defer globalLock.Unlock() if _, ok := blockControllerMap[blockId]; ok { - return + // already running + return nil } bc := &BlockController{ Lock: &sync.Mutex{}, @@ -249,7 +221,17 @@ func StartBlockController(blockId string, bdata *wstore.Block) { InputCh: make(chan BlockCommand), } blockControllerMap[blockId] = bc - go bc.Run(bdata) + go bc.Run(blockData) + return nil +} + +func StopBlockController(blockId string) { + bc := GetBlockController(blockId) + if bc == nil { + return + } + bc.Close() + close(bc.InputCh) } func GetBlockController(blockId string) *BlockController { diff --git a/pkg/eventbus/eventbus.go b/pkg/eventbus/eventbus.go index 8300d76b8..41a8933f2 100644 --- a/pkg/eventbus/eventbus.go +++ b/pkg/eventbus/eventbus.go @@ -5,11 +5,13 @@ package eventbus import ( "errors" + "fmt" "log" "runtime/debug" "sync" "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wavetermdev/thenextwave/pkg/waveobj" ) const EventBufferSize = 50 @@ -24,9 +26,16 @@ type WindowEvent struct { Event application.WailsEvent } +type WindowWatchData struct { + Window *application.WebviewWindow + WaveWindowId string + WailsWindowId uint + WatchedORefs map[waveobj.ORef]bool +} + var globalLock = &sync.Mutex{} var wailsApp *application.App -var wailsWindowMap = make(map[uint]*application.WebviewWindow) +var wailsWindowMap = make(map[uint]*WindowWatchData) func Start() { go processEvents() @@ -42,10 +51,18 @@ func RegisterWailsApp(app *application.App) { wailsApp = app } -func RegisterWailsWindow(window *application.WebviewWindow) { +func RegisterWailsWindow(window *application.WebviewWindow, windowId string) { globalLock.Lock() defer globalLock.Unlock() - wailsWindowMap[window.ID()] = window + if _, found := wailsWindowMap[window.ID()]; found { + panic(fmt.Errorf("wails window already registered with eventbus: %d", window.ID())) + } + wailsWindowMap[window.ID()] = &WindowWatchData{ + Window: window, + WailsWindowId: window.ID(), + WaveWindowId: "", + WatchedORefs: make(map[waveobj.ORef]bool), + } } func UnregisterWailsWindow(windowId uint) { @@ -56,18 +73,18 @@ func UnregisterWailsWindow(windowId uint) { func emitEventToWindow(event WindowEvent) { globalLock.Lock() - window := wailsWindowMap[event.WindowId] + wdata := wailsWindowMap[event.WindowId] globalLock.Unlock() - if window != nil { - window.DispatchWailsEvent(&event.Event) + if wdata != nil { + wdata.Window.DispatchWailsEvent(&event.Event) } } func emitEventToAllWindows(event *application.WailsEvent) { globalLock.Lock() wins := make([]*application.WebviewWindow, 0, len(wailsWindowMap)) - for _, window := range wailsWindowMap { - wins = append(wins, window) + for _, wdata := range wailsWindowMap { + wins = append(wins, wdata.Window) } globalLock.Unlock() for _, window := range wins { @@ -79,6 +96,25 @@ func SendEvent(event application.WailsEvent) { EventCh <- event } +func findWindowIdsByORef(oref waveobj.ORef) []uint { + globalLock.Lock() + defer globalLock.Unlock() + var ids []uint + for _, wdata := range wailsWindowMap { + if wdata.WatchedORefs[oref] { + ids = append(ids, wdata.WailsWindowId) + } + } + return ids +} + +func SendORefEvent(oref waveobj.ORef, event application.WailsEvent) { + wins := findWindowIdsByORef(oref) + for _, windowId := range wins { + SendWindowEvent(windowId, event) + } +} + func SendEventNonBlocking(event application.WailsEvent) error { select { case EventCh <- event: diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 615cd7f67..e5fe25229 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -4,49 +4,17 @@ package blockservice import ( - "context" "fmt" "strings" "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" - "github.com/wavetermdev/thenextwave/pkg/wstore" ) type BlockService struct{} 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") - } - if rtOpts == nil { - return nil, fmt.Errorf("runtime options is nil") - } - blockData, err := blockcontroller.CreateBlock(ctx, bdef, rtOpts) - if err != nil { - return nil, fmt.Errorf("error creating block: %w", err) - } - return blockData, nil -} - -func (bs *BlockService) CloseBlock(blockId string) { - blockcontroller.CloseBlock(blockId) -} - -func (bs *BlockService) GetBlockData(blockId string) (*wstore.Block, error) { - ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) - defer cancelFn() - blockData, err := wstore.DBGet[*wstore.Block](ctx, blockId) - if err != nil { - return nil, fmt.Errorf("error getting block data: %w", err) - } - return blockData, nil -} - func (bs *BlockService) SendCommand(blockId string, cmdMap map[string]any) error { cmd, err := blockcontroller.ParseCmdMap(cmdMap) if err != nil { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index cfa612c9e..1aa7f42e3 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -109,3 +110,25 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string) } return updatesRtn(ctx, nil) } + +func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (any, error) { + if uiContext.ActiveTabId == "" { + return nil, fmt.Errorf("no active tab") + } + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + ctx = wstore.ContextWithUpdates(ctx) + blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts) + if err != nil { + return nil, fmt.Errorf("error creating block: %w", err) + } + if blockData.Controller != "" { + err = blockcontroller.StartBlockController(ctx, blockData.OID) + if err != nil { + return nil, fmt.Errorf("error starting block controller: %w", err) + } + } + rtn := make(map[string]any) + rtn["blockid"] = blockData.OID + return updatesRtn(ctx, rtn) +} diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index 3bb9f078c..8d92d3a43 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -168,7 +168,8 @@ func (update WaveObjUpdate) MarshalJSON() ([]byte, error) { } type UIContext struct { - WindowId string `json:"windowid"` + WindowId string `json:"windowid"` + ActiveTabId string `json:"activetabid"` } type Client struct { @@ -239,7 +240,7 @@ type FileDef struct { } type BlockDef struct { - Controller string `json:"controller"` + Controller string `json:"controller,omitempty"` View string `json:"view,omitempty"` Files map[string]*FileDef `json:"files,omitempty"` Meta map[string]any `json:"meta,omitempty"` @@ -317,6 +318,28 @@ func SetActiveTab(ctx context.Context, windowId string, tabId string) error { }) } +func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts *RuntimeOpts) (*Block, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { + tab, _ := DBGet[*Tab](tx.Context(), tabId) + if tab == nil { + return nil, fmt.Errorf("tab not found: %q", tabId) + } + blockId := uuid.New().String() + blockData := &Block{ + OID: blockId, + BlockDef: blockDef, + Controller: blockDef.Controller, + View: blockDef.View, + RuntimeOpts: rtOpts, + Meta: blockDef.Meta, + } + DBInsert(tx.Context(), blockData) + tab.BlockIds = append(tab.BlockIds, blockId) + DBUpdate(tx.Context(), tab) + return blockData, nil + }) +} + func EnsureInitialData() error { // does not need to run in a transaction since it is called on startup ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)