diff --git a/cmd/generate/main-generate.go b/cmd/generate/main-generate.go new file mode 100644 index 000000000..54b4bd6f9 --- /dev/null +++ b/cmd/generate/main-generate.go @@ -0,0 +1,26 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "reflect" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +func main() { + tsTypesMap := make(map[reflect.Type]string) + var waveObj waveobj.WaveObj + waveobj.GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap) + waveobj.GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsTypesMap) + for _, rtype := range wstore.AllWaveObjTypes() { + waveobj.GenerateTSType(rtype, tsTypesMap) + } + for _, ts := range tsTypesMap { + fmt.Print(ts) + fmt.Print("\n") + } +} diff --git a/cmd/main-wsh.go b/cmd/wsh/main-wsh.go similarity index 100% rename from cmd/main-wsh.go rename to cmd/wsh/main-wsh.go diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index f615f4b95..3089e7575 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -1,15 +1,18 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import * as React from "react"; import * as jotai from "jotai"; -import { atomFamily } from "jotai/utils"; +import * as jotaiUtils from "jotai/utils"; import { v4 as uuidv4 } from "uuid"; import * as rxjs from "rxjs"; import type { WailsEvent } from "@wailsio/runtime/types/events"; import { Events } from "@wailsio/runtime"; import { produce } from "immer"; import { BlockService } from "@/bindings/blockservice"; +import { ObjectService } from "@/bindings/objectservice"; import * as wstore from "@/gopkg/wstore"; +import { Call as $Call } from "@wailsio/runtime"; const globalStore = jotai.createStore(); @@ -105,4 +108,93 @@ function removeBlockFromTab(tabId: string, blockId: string) { BlockService.CloseBlock(blockId); } -export { globalStore, atoms, getBlockSubject, addBlockIdToTab, blockDataMap, useBlockAtom, removeBlockFromTab }; +function GetObject(oref: string): Promise { + let prtn = $Call.ByName( + "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", + oref + ); + return prtn; +} + +type WaveObjectHookData = { + oref: string; +}; + +type WaveObjectValue = { + pendingPromise: Promise; + value: T; + loading: boolean; +}; + +const waveObjectValueCache = new Map>(); +let waveObjectAtomCache = new WeakMap>(); + +function clearWaveObjectCache() { + waveObjectValueCache.clear(); + waveObjectAtomCache = new WeakMap>(); +} + +function createWaveObjectAtom(oref: string): jotai.Atom<[T, boolean]> { + let cacheVal: WaveObjectValue = waveObjectValueCache.get(oref); + if (cacheVal == null) { + cacheVal = { pendingPromise: null, value: null, loading: true }; + cacheVal.pendingPromise = GetObject(oref).then((val) => { + cacheVal.value = val; + cacheVal.loading = false; + cacheVal.pendingPromise = null; + }); + waveObjectValueCache.set(oref, cacheVal); + } + return jotai.atom( + (get) => { + return [cacheVal.value, cacheVal.loading]; + }, + (get, set, newVal: T) => { + cacheVal.value = newVal; + } + ); +} + +function useWaveObjectValue(oref: string): [T, boolean] { + const objRef = React.useRef(null); + if (objRef.current == null) { + objRef.current = { oref: oref }; + } + const objHookData = objRef.current; + let objAtom = waveObjectAtomCache.get(objHookData); + if (objAtom == null) { + objAtom = createWaveObjectAtom(oref); + waveObjectAtomCache.set(objHookData, objAtom); + } + const atomVal = jotai.useAtomValue(objAtom); + return [atomVal[0], atomVal[1]]; +} + +function useWaveObject(oref: string): [T, boolean, (T) => void] { + const objRef = React.useRef(null); + if (objRef.current == null) { + objRef.current = { oref: oref }; + } + const objHookData = objRef.current; + let objAtom = waveObjectAtomCache.get(objHookData); + if (objAtom == null) { + objAtom = createWaveObjectAtom(oref); + waveObjectAtomCache.set(objHookData, objAtom); + } + const [atomVal, setAtomVal] = jotai.useAtom(objAtom); + return [atomVal[0], atomVal[1], setAtomVal]; +} + +export { + globalStore, + atoms, + getBlockSubject, + addBlockIdToTab, + blockDataMap, + useBlockAtom, + removeBlockFromTab, + GetObject, + useWaveObject, + useWaveObjectValue, + clearWaveObjectCache, +}; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index d12626e2e..c7111e1c5 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,6 +1,97 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -declare global {} +declare global { + type ORef = { + otype: string; + oid: string; + }; + + type Block = { + otype: string; + oid: string; + version: number; + blockdef: BlockDef; + controller: string; + view: string; + meta?: { [key: string]: any }; + runtimeopts?: RuntimeOpts; + }; + + type BlockDef = { + controller: string; + view?: string; + files?: { [key: string]: FileDef }; + meta?: { [key: string]: any }; + }; + + type FileDef = { + filetype?: string; + path?: string; + url?: string; + content?: string; + meta?: { [key: string]: any }; + }; + + type TermSize = { + rows: number; + cols: number; + }; + + type Client = { + otype: string; + oid: string; + version: number; + mainwindowid: string; + }; + + type Tab = { + otype: string; + oid: string; + version: number; + name: string; + blockids: string[]; + }; + + type Point = { + x: number; + y: number; + }; + + type WinSize = { + width: number; + height: number; + }; + + type Workspace = { + otype: string; + oid: string; + version: number; + name: string; + tabids: string[]; + }; + + type RuntimeOpts = { + termsize?: TermSize; + winsize?: WinSize; + }; + + type WaveObj = { + otype: string; + oid: string; + }; + + type Window = { + otype: string; + oid: string; + version: number; + workspaceid: string; + activetabid: string; + activeblockmap: { [key: string]: string }; + pos: Point; + winsize: WinSize; + lastfocusts: number; + }; +} export {}; diff --git a/main.go b/main.go index 9c2532355..7b976f801 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "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/objectservice" "github.com/wavetermdev/thenextwave/pkg/wavebase" "github.com/wavetermdev/thenextwave/pkg/wstore" @@ -131,6 +132,7 @@ func main() { application.NewService(&fileservice.FileService{}), application.NewService(&blockservice.BlockService{}), application.NewService(&clientservice.ClientService{}), + application.NewService(&objectservice.ObjectService{}), }, Icon: appIcon, Assets: application.AssetOptions{ diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go new file mode 100644 index 000000000..96995599d --- /dev/null +++ b/pkg/service/objectservice/objectservice.go @@ -0,0 +1,55 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package objectservice + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +type ObjectService struct{} + +const DefaultTimeout = 2 * time.Second + +func parseORef(oref string) (*waveobj.ORef, error) { + fields := strings.Split(oref, ":") + if len(fields) != 2 { + return nil, fmt.Errorf("invalid object reference: %q", oref) + } + return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil +} + +func (svc *ObjectService) GetObject(orefStr string) (any, error) { + oref, err := parseORef(orefStr) + if err != nil { + return nil, err + } + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + obj, err := wstore.DBGetORef(ctx, *oref) + if err != nil { + return nil, fmt.Errorf("error getting object: %w", err) + } + return obj, nil +} + +func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) + defer cancelFn() + + var orefArr []waveobj.ORef + for _, orefStr := range orefStrArr { + orefObj, err := parseORef(orefStr) + if err != nil { + return nil, err + } + orefArr = append(orefArr, *orefObj) + } + return wstore.DBSelectORefs(ctx, orefArr) +} diff --git a/pkg/waveobj/waveobj.go b/pkg/waveobj/waveobj.go index fb773fdf6..0b1a54211 100644 --- a/pkg/waveobj/waveobj.go +++ b/pkg/waveobj/waveobj.go @@ -23,6 +23,11 @@ const ( VersionGoFieldName = "Version" ) +type ORef struct { + OType string `json:"otype"` + OID string `json:"oid"` +} + type WaveObj interface { GetOType() string // should not depend on object state (should work with nil value) } @@ -36,35 +41,37 @@ type waveObjDesc struct { var waveObjMap = sync.Map{} var waveObjRType = reflect.TypeOf((*WaveObj)(nil)).Elem() -func RegisterType[T WaveObj]() { - var waveObj T +func RegisterType(rtype reflect.Type) { + if rtype.Kind() != reflect.Ptr { + panic(fmt.Sprintf("wave object must be a pointer for %v", rtype)) + } + if !rtype.Implements(waveObjRType) { + panic(fmt.Sprintf("wave object must implement WaveObj for %v", rtype)) + } + waveObj := reflect.Zero(rtype).Interface().(WaveObj) otype := waveObj.GetOType() if otype == "" { - panic(fmt.Sprintf("otype is empty for %T", waveObj)) - } - rtype := reflect.TypeOf(waveObj) - if rtype.Kind() != reflect.Ptr { - panic(fmt.Sprintf("wave object must be a pointer for %T", waveObj)) + panic(fmt.Sprintf("otype is empty for %v", rtype)) } oidField, found := rtype.Elem().FieldByName(OIDGoFieldName) if !found { - panic(fmt.Sprintf("missing OID field for %T", waveObj)) + panic(fmt.Sprintf("missing OID field for %v", rtype)) } if oidField.Type.Kind() != reflect.String { - panic(fmt.Sprintf("OID field must be string for %T", waveObj)) + panic(fmt.Sprintf("OID field must be string for %v", rtype)) } if oidField.Tag.Get("json") != OIDKeyName { - panic(fmt.Sprintf("OID field json tag must be %q for %T", OIDKeyName, waveObj)) + panic(fmt.Sprintf("OID field json tag must be %q for %v", OIDKeyName, rtype)) } versionField, found := rtype.Elem().FieldByName(VersionGoFieldName) if !found { - panic(fmt.Sprintf("missing Version field for %T", waveObj)) + panic(fmt.Sprintf("missing Version field for %v", rtype)) } if versionField.Type.Kind() != reflect.Int { - panic(fmt.Sprintf("Version field must be int for %T", waveObj)) + panic(fmt.Sprintf("Version field must be int for %v", rtype)) } if versionField.Tag.Get("json") != VersionKeyName { - panic(fmt.Sprintf("Version field json tag must be %q for %T", VersionKeyName, waveObj)) + panic(fmt.Sprintf("Version field json tag must be %q for %v", VersionKeyName, rtype)) } _, found = waveObjMap.Load(otype) if found { @@ -286,16 +293,16 @@ func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { subTypes = append(subTypes, fieldSubTypes...) buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType)) } - buf.WriteString("}\n") + buf.WriteString("};\n") return buf.String(), subTypes } func GenerateWaveObjTSType() string { var buf bytes.Buffer - buf.WriteString("type WaveObj {\n") + buf.WriteString("type WaveObj = {\n") buf.WriteString(" otype: string;\n") buf.WriteString(" oid: string;\n") - buf.WriteString("}\n") + buf.WriteString("};\n") return buf.String() } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index d71e92677..3e54986d9 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -6,6 +6,7 @@ package wstore import ( "context" "fmt" + "reflect" "time" "github.com/google/uuid" @@ -19,11 +20,9 @@ var TabMap = ds.NewSyncMap[*Tab]() var BlockMap = ds.NewSyncMap[*Block]() func init() { - waveobj.RegisterType[*Client]() - waveobj.RegisterType[*Window]() - waveobj.RegisterType[*Workspace]() - waveobj.RegisterType[*Tab]() - waveobj.RegisterType[*Block]() + for _, rtype := range AllWaveObjTypes() { + waveobj.RegisterType(rtype) + } } type Client struct { @@ -36,6 +35,16 @@ func (*Client) GetOType() string { return "client" } +func AllWaveObjTypes() []reflect.Type { + return []reflect.Type{ + reflect.TypeOf(&Client{}), + reflect.TypeOf(&Window{}), + reflect.TypeOf(&Workspace{}), + reflect.TypeOf(&Tab{}), + reflect.TypeOf(&Block{}), + } +} + // stores the ui-context of the window // workspaceid, active tab, active block within each tab, window size, etc. type Window struct { @@ -147,6 +156,10 @@ func CreateWorkspace() (*Workspace, error) { return ws, nil } +func GetObject(otype string, oid string) (waveobj.WaveObj, error) { + return nil, nil +} + func EnsureInitialData() error { ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) defer cancelFn() diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go index 3ae151239..393ac6284 100644 --- a/pkg/wstore/wstore_dbops.go +++ b/pkg/wstore/wstore_dbops.go @@ -14,9 +14,18 @@ func waveObjTableName(w waveobj.WaveObj) string { return "db_" + w.GetOType() } +func tableNameFromOType(otype string) string { + return "db_" + otype +} + func tableNameGen[T waveobj.WaveObj]() string { var zeroObj T - return "db_" + zeroObj.GetOType() + return tableNameFromOType(zeroObj.GetOType()) +} + +func getOTypeGen[T waveobj.WaveObj]() string { + var zeroObj T + return zeroObj.GetOType() } func DBGetCount[T waveobj.WaveObj](ctx context.Context) (int, error) { @@ -33,13 +42,26 @@ type idDataType struct { Data []byte } +func genericCastWithErr[T any](v any, err error) (T, error) { + if err != nil { + var zeroVal T + return zeroVal, err + } + return v.(T), err +} + func DBGetSingleton[T waveobj.WaveObj](ctx context.Context) (T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { - table := tableNameGen[T]() + rtn, err := DBGetSingletonByType(ctx, getOTypeGen[T]()) + return genericCastWithErr[T](rtn, err) +} + +func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (waveobj.WaveObj, error) { + table := tableNameFromOType(otype) query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table) var row idDataType tx.Get(&row, query) - rtn, err := waveobj.FromJsonGen[T](row.Data) + rtn, err := waveobj.FromJson(row.Data) if err != nil { return rtn, err } @@ -49,12 +71,17 @@ func DBGetSingleton[T waveobj.WaveObj](ctx context.Context) (T, error) { } func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (T, error) { - table := tableNameGen[T]() + rtn, err := DBGetORef(ctx, waveobj.ORef{OType: getOTypeGen[T](), OID: id}) + return genericCastWithErr[T](rtn, err) +} + +func DBGetORef(ctx context.Context, oref waveobj.ORef) (waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (waveobj.WaveObj, error) { + table := tableNameFromOType(oref.OType) query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid = ?", table) var row idDataType - tx.Get(&row, query, id) - rtn, err := waveobj.FromJsonGen[T](row.Data) + tx.Get(&row, query, oref.OID) + rtn, err := waveobj.FromJson(row.Data) if err != nil { return rtn, err } @@ -63,31 +90,58 @@ func DBGet[T waveobj.WaveObj](ctx context.Context, id string) (T, error) { }) } -func DBSelectMap[T waveobj.WaveObj](ctx context.Context, ids []string) (map[string]T, error) { - return WithTxRtn(ctx, func(tx *TxWrap) (map[string]T, error) { - table := tableNameGen[T]() - var rows []idDataType +func dbSelectOIDs(ctx context.Context, otype string, oids []string) ([]waveobj.WaveObj, error) { + return WithTxRtn(ctx, func(tx *TxWrap) ([]waveobj.WaveObj, error) { + table := tableNameFromOType(otype) query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table) - tx.Select(&rows, query, ids) - rtnMap := make(map[string]T) + var rows []idDataType + tx.Select(&rows, query, oids) + rtn := make([]waveobj.WaveObj, 0, len(rows)) for _, row := range rows { - if row.OId == "" || len(row.Data) == 0 { - continue - } - waveObj, err := waveobj.FromJsonGen[T](row.Data) + waveObj, err := waveobj.FromJson(row.Data) if err != nil { return nil, err } waveobj.SetVersion(waveObj, row.Version) - rtnMap[row.OId] = waveObj + rtn = append(rtn, waveObj) } - return rtnMap, nil + return rtn, nil }) } -func DBDelete[T waveobj.WaveObj](ctx context.Context, id string) error { +func DBSelectORefs(ctx context.Context, orefs []waveobj.ORef) ([]waveobj.WaveObj, error) { + oidsByType := make(map[string][]string) + for _, oref := range orefs { + oidsByType[oref.OType] = append(oidsByType[oref.OType], oref.OID) + } + return WithTxRtn(ctx, func(tx *TxWrap) ([]waveobj.WaveObj, error) { + rtn := make([]waveobj.WaveObj, 0, len(orefs)) + for otype, oids := range oidsByType { + rtnArr, err := dbSelectOIDs(tx.Context(), otype, oids) + if err != nil { + return nil, err + } + rtn = append(rtn, rtnArr...) + } + return rtn, nil + }) +} + +func DBSelectMap[T waveobj.WaveObj](ctx context.Context, ids []string) (map[string]T, error) { + rtnArr, err := dbSelectOIDs(ctx, getOTypeGen[T](), ids) + if err != nil { + return nil, err + } + rtnMap := make(map[string]T) + for _, obj := range rtnArr { + rtnMap[waveobj.GetOID(obj)] = obj.(T) + } + return rtnMap, nil +} + +func DBDelete(ctx context.Context, otype string, id string) error { return WithTx(ctx, func(tx *TxWrap) error { - table := tableNameGen[T]() + table := tableNameFromOType(otype) query := fmt.Sprintf("DELETE FROM %s WHERE oid = ?", table) tx.Exec(query, id) return nil @@ -111,7 +165,7 @@ func DBUpdate(ctx context.Context, val waveobj.WaveObj) error { }) } -func DBInsert[T waveobj.WaveObj](ctx context.Context, val T) error { +func DBInsert(ctx context.Context, val waveobj.WaveObj) error { oid := waveobj.GetOID(val) if oid == "" { return fmt.Errorf("cannot insert %T value with empty id", val)