port to electron (#33)

This commit is contained in:
Mike Sawka
2024-06-11 17:42:10 -07:00
committed by GitHub
parent 9f32a53485
commit 1874d9a252
62 changed files with 6498 additions and 1615 deletions
+2
View File
@@ -1,6 +1,7 @@
.task
frontend/dist
dist/
dist-dev/
frontend/node_modules
node_modules/
frontend/bindings
@@ -11,6 +12,7 @@ bin/
*.exe
.DS_Store
*~
out/
# Yarn Modern
.pnp.*
+431
View File
@@ -0,0 +1,431 @@
version: "3"
vars:
APP_NAME: "NextWave"
BIN_DIR: "bin"
VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}"
tasks:
## -------------------------- Build -------------------------- ##
build:
summary: Builds the application
cmds:
# Build for current OS
- task: build:{{OS}}
# Uncomment to build for specific OSes
# - task: build:linux
# - task: build:windows
# - task: build:darwin
## ------> Windows <-------
build:windows:
summary: Builds the application for Windows
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
- task: generate:syso
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: windows
CGO_ENABLED: 0
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:windows:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
PRODUCTION: "true"
build:windows:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
PRODUCTION: "true"
build:windows:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
build:windows:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
## ------> Darwin <-------
build:darwin:
summary: Creates a production build of the application
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: darwin
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:darwin:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
PRODUCTION: "true"
build:darwin:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
PRODUCTION: "true"
build:darwin:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
build:darwin:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
## ------> Linux <-------
build:linux:
summary: Builds the application for Linux
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/w2
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:linux:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
PRODUCTION: "true"
build:linux:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
PRODUCTION: "true"
build:linux:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
build:linux:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
## -------------------------- Package -------------------------- ##
package:
summary: Packages a production build of the application into a bundle
cmds:
# Package for current OS
- task: package:{{OS}}
# Package for specific os/arch
# - task: package:darwin:arm64
# - task: package:darwin:amd64
# - task: package:windows:arm64
# - task: package:windows:amd64
## ------> Windows <------
package:windows:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: create:nsis:installer
vars:
ARCH: "{{.ARCH}}"
vars:
ARCH: "{{.ARCH | default ARCH}}"
package:windows:arm64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: arm64
package:windows:amd64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: amd64
generate:syso:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso
vars:
ARCH: "{{.ARCH | default ARCH}}"
create:nsis:installer:
summary: Creates an NSIS installer
label: "NSIS Installer ({{.ARCH}})"
dir: build/nsis
sources:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}.exe"
generates:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}-{{.ARCH}}-installer.exe"
deps:
- task: build:windows
vars:
PRODUCTION: "true"
ARCH: "{{.ARCH}}"
cmds:
- makensis -DARG_WAILS_'{{.ARG_FLAG}}'_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
vars:
ARCH: "{{.ARCH | default ARCH}}"
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
## ------> Darwin <------
package:darwin:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin]
deps:
- task: build:darwin
vars:
PRODUCTION: "true"
cmds:
- task: create:app:bundle
package:darwin:arm64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/arm64]
deps:
- task: package:darwin
vars:
ARCH: arm64
package:darwin:amd64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/amd64]
deps:
- task: package:darwin
vars:
ARCH: amd64
create:app:bundle:
summary: Creates an `.app` bundle
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
## ------> Linux <------
package:linux:
summary: Packages a production build of the application for Linux
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
cmds:
- task: create:appimage
create:appimage:
summary: Creates an AppImage
dir: build/appimage
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
- task: generate:linux:dotdesktop
cmds:
# Copy binary + icon to appimage dir
- cp {{.APP_BINARY}} {{.APP_NAME}}
- cp ../appicon.png appicon.png
# Generate AppImage
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
vars:
APP_NAME: "{{.APP_NAME}}"
APP_BINARY: "../../bin/{{.APP_NAME}}"
ICON: "../appicon.png"
DESKTOP_FILE: "{{.APP_NAME}}.desktop"
OUTPUT_DIR: "../../bin"
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
sources:
- "appicon.png"
generates:
- "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
cmds:
- mkdir -p {{.ROOT_DIR}}/build/appimage
# Run `wails3 generate .desktop -help` for all the options
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
vars:
APP_NAME: "{{.APP_NAME}}"
EXEC: "{{.APP_NAME}}"
ICON: "appicon"
CATEGORIES: "Development;"
OUTPUTFILE: "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
## -------------------------- Misc -------------------------- ##
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build
sources:
- "appicon.png"
generates:
- "icons.icns"
- "icons.ico"
method: timestamp
cmds:
# Generates both .ico and .icns files
# commented out for now
# - wails3 generate icons -input appicon.png
install:frontend:deps:
summary: Install frontend dependencies
sources:
- package.json
- yarn.lock
generates:
- node_modules/*
preconditions:
- sh: yarn --version
msg: "Looks like yarn isn't installed."
cmds:
- yarn
build:frontend:
summary: Build the frontend project
sources:
- "**/*"
generates:
- dist/*
deps:
- install:frontend:deps
- generate:bindings
cmds:
- yarn build
generate:bindings:
summary: Generates bindings for the frontend
sources:
- "**/*.go"
generates:
- "frontend/bindings/**/*"
cmds:
- wails3 generate bindings -silent -ts
# - wails3 generate bindings -silent
go:mod:tidy:
summary: Runs `go mod tidy`
internal: true
generates:
- go.sum
sources:
- go.mod
cmds:
- go mod tidy
# ----------------------- dev ----------------------- #
run:
summary: Runs the application
cmds:
- task: run:{{OS}}
run:windows:
cmds:
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
run:linux:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
run:darwin:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/devmode.config.yaml -port {{.VITE_PORT}}
dev:reload:
summary: Reloads the application
cmds:
- task: run
+19 -401
View File
@@ -3,386 +3,38 @@ version: "3"
vars:
APP_NAME: "NextWave"
BIN_DIR: "bin"
VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}"
VERSION: "0.1.0"
tasks:
## -------------------------- Build -------------------------- ##
build:
summary: Builds the application
generate:
cmds:
# Build for current OS
- task: build:{{OS}}
# Uncomment to build for specific OSes
# - task: build:linux
# - task: build:windows
# - task: build:darwin
## ------> Windows <-------
build:windows:
summary: Builds the application for Windows
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
- task: generate:syso
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: windows
CGO_ENABLED: 0
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:windows:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
PRODUCTION: "true"
build:windows:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
PRODUCTION: "true"
build:windows:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
build:windows:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
## ------> Darwin <-------
build:darwin:
summary: Creates a production build of the application
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: darwin
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:darwin:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
PRODUCTION: "true"
build:darwin:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
PRODUCTION: "true"
build:darwin:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
build:darwin:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
## ------> Linux <-------
build:linux:
summary: Builds the application for Linux
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/w2
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:linux:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
PRODUCTION: "true"
build:linux:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
PRODUCTION: "true"
build:linux:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
build:linux:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
## -------------------------- Package -------------------------- ##
package:
summary: Packages a production build of the application into a bundle
cmds:
# Package for current OS
- task: package:{{OS}}
# Package for specific os/arch
# - task: package:darwin:arm64
# - task: package:darwin:amd64
# - task: package:windows:arm64
# - task: package:windows:amd64
## ------> Windows <------
package:windows:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: create:nsis:installer
vars:
ARCH: "{{.ARCH}}"
vars:
ARCH: "{{.ARCH | default ARCH}}"
package:windows:arm64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: arm64
package:windows:amd64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: amd64
generate:syso:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso
vars:
ARCH: "{{.ARCH | default ARCH}}"
create:nsis:installer:
summary: Creates an NSIS installer
label: "NSIS Installer ({{.ARCH}})"
dir: build/nsis
- go run cmd/generate/main-generate.go
sources:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}.exe"
generates:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}-{{.ARCH}}-installer.exe"
deps:
- task: build:windows
vars:
PRODUCTION: "true"
ARCH: "{{.ARCH}}"
- "cmd/generate/*.go"
- "pkg/service/**/*.go"
- "pkg/wstore/*.go"
webpack:
cmds:
- makensis -DARG_WAILS_'{{.ARG_FLAG}}'_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
vars:
ARCH: "{{.ARCH | default ARCH}}"
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
- yarn run webpack --watch --env dev
## ------> Darwin <------
package:darwin:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin]
deps:
- task: build:darwin
vars:
PRODUCTION: "true"
electron:
cmds:
- task: create:app:bundle
package:darwin:arm64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/arm64]
- WAVETERM_DEV=1 yarn run electron dist-dev/emain.js
deps:
- task: package:darwin
vars:
ARCH: arm64
- build:server
package:darwin:amd64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/amd64]
deps:
- task: package:darwin
vars:
ARCH: amd64
create:app:bundle:
summary: Creates an `.app` bundle
build:server:
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
## ------> Linux <------
package:linux:
summary: Packages a production build of the application for Linux
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
cmds:
- task: create:appimage
create:appimage:
summary: Creates an AppImage
dir: build/appimage
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
- task: generate:linux:dotdesktop
cmds:
# Copy binary + icon to appimage dir
- cp {{.APP_BINARY}} {{.APP_NAME}}
- cp ../appicon.png appicon.png
# Generate AppImage
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
vars:
APP_NAME: "{{.APP_NAME}}"
APP_BINARY: "../../bin/{{.APP_NAME}}"
ICON: "../appicon.png"
DESKTOP_FILE: "{{.APP_NAME}}.desktop"
OUTPUT_DIR: "../../bin"
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
- go build -o bin/wavesrv cmd/server/main-server.go
sources:
- "appicon.png"
- "cmd/server/*.go"
- "pkg/**/*.go"
generates:
- "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
cmds:
- mkdir -p {{.ROOT_DIR}}/build/appimage
# Run `wails3 generate .desktop -help` for all the options
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
vars:
APP_NAME: "{{.APP_NAME}}"
EXEC: "{{.APP_NAME}}"
ICON: "appicon"
CATEGORIES: "Development;"
OUTPUTFILE: "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
## -------------------------- Misc -------------------------- ##
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build
sources:
- "appicon.png"
generates:
- "icons.icns"
- "icons.ico"
method: timestamp
cmds:
# Generates both .ico and .icns files
# commented out for now
# - wails3 generate icons -input appicon.png
install:frontend:deps:
summary: Install frontend dependencies
sources:
- package.json
- yarn.lock
generates:
- node_modules/*
preconditions:
- sh: yarn --version
msg: "Looks like yarn isn't installed."
cmds:
- yarn
build:frontend:
summary: Build the frontend project
sources:
- "**/*"
generates:
- dist/*
- bin/wavesrv
deps:
- install:frontend:deps
- generate:bindings
cmds:
- yarn build
generate:bindings:
summary: Generates bindings for the frontend
sources:
- "**/*.go"
generates:
- "frontend/bindings/**/*"
cmds:
- wails3 generate bindings -silent -ts
# - wails3 generate bindings -silent
- go:mod:tidy
go:mod:tidy:
summary: Runs `go mod tidy`
@@ -394,38 +46,4 @@ tasks:
cmds:
- go mod tidy
# ----------------------- dev ----------------------- #
run:
summary: Runs the application
cmds:
- task: run:{{OS}}
run:windows:
cmds:
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
run:linux:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
run:darwin:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/devmode.config.yaml -port {{.VITE_PORT}}
dev:reload:
summary: Reloads the application
cmds:
- task: run
+77 -12
View File
@@ -5,22 +5,87 @@ package main
import (
"fmt"
"os"
"reflect"
"sort"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/tsgen"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
)
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)
func generateTypesFile() error {
fd, err := os.Create("frontend/types/gotypes.d.ts")
if err != nil {
return err
}
for _, ts := range tsTypesMap {
fmt.Print(ts)
fmt.Print("\n")
defer fd.Close()
fmt.Fprintf(os.Stderr, "generating types file to %s\n", fd.Name())
tsTypesMap := make(map[reflect.Type]string)
tsgen.GenerateWaveObjTypes(tsTypesMap)
err = tsgen.GenerateServiceTypes(tsTypesMap)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating service types: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n")
fmt.Fprintf(fd, "declare global {\n\n")
var keys []reflect.Type
for key := range tsTypesMap {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
iname, _ := tsgen.TypeToTSType(keys[i])
jname, _ := tsgen.TypeToTSType(keys[j])
return iname < jname
})
for _, key := range keys {
tsCode := tsTypesMap[key]
istr := utilfn.IndentString(" ", tsCode)
fmt.Fprint(fd, istr)
}
fmt.Fprintf(fd, "}\n\n")
fmt.Fprintf(fd, "export {}\n")
return nil
}
func generateServicesFile() error {
fd, err := os.Create("frontend/app/store/services.ts")
if err != nil {
return err
}
defer fd.Close()
fmt.Fprintf(os.Stderr, "generating services file to %s\n", fd.Name())
fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n")
fmt.Fprintf(fd, "import * as WOS from \"./wos\";\n\n")
orderedKeys := utilfn.GetOrderedMapKeys(service.ServiceMap)
for _, serviceName := range orderedKeys {
serviceObj := service.ServiceMap[serviceName]
svcStr := tsgen.GenerateServiceClass(serviceName, serviceObj)
fmt.Fprint(fd, svcStr)
fmt.Fprint(fd, "\n")
}
return nil
}
func main() {
err := service.ValidateServiceMap()
if err != nil {
fmt.Fprintf(os.Stderr, "Error validating service map: %v\n", err)
os.Exit(1)
}
err = generateTypesFile()
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating types file: %v\n", err)
os.Exit(1)
}
err = generateServicesFile()
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating services file: %v\n", err)
os.Exit(1)
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"strconv"
"syscall"
"time"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/web"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
const ReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID"
func doShutdown(reason string) {
log.Printf("shutting down: %s\n", reason)
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
// TODO deal with flush in progress
filestore.WFS.FlushCache(ctx)
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}
func installShutdownSignalHandlers() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
go func() {
for sig := range sigCh {
doShutdown(fmt.Sprintf("got signal %v", sig))
break
}
}()
}
func main() {
err := service.ValidateServiceMap()
if err != nil {
log.Printf("error validating service map: %v\n", err)
return
}
err = wavebase.EnsureWaveHomeDir()
if err != nil {
log.Printf("error ensuring wave home dir: %v\n", err)
return
}
waveLock, err := wavebase.AcquireWaveLock()
if err != nil {
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)
return
}
log.Printf("wave home dir: %s\n", wavebase.GetWaveHomeDir())
err = filestore.InitFilestore()
if err != nil {
log.Printf("error initializing filestore: %v\n", err)
return
}
err = wstore.InitWStore()
if err != nil {
log.Printf("error initializing wstore: %v\n", err)
return
}
err = wstore.EnsureInitialData()
if err != nil {
log.Printf("error ensuring initial data: %v\n", err)
return
}
installShutdownSignalHandlers()
go web.RunWebSocketServer()
go func() {
time.Sleep(30 * time.Millisecond)
pidStr := os.Getenv(ReadySignalPidVarName)
if pidStr != "" {
pid, err := strconv.Atoi(pidStr)
if err == nil {
syscall.Kill(pid, syscall.SIGUSR1)
}
}
}()
web.RunWebServer() // blocking
runtime.KeepAlive(waveLock)
}
+243
View File
@@ -0,0 +1,243 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";
import * as child_process from "node:child_process";
import * as path from "path";
import { debounce } from "throttle-debounce";
import * as services from "../frontend/app/store/services";
const electronApp = electron.app;
const isDev = true;
const WaveAppPathVarName = "WAVETERM_APP_PATH";
const WaveDevVarName = "WAVETERM_DEV";
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8190";
const ProdServerEndpoint = "http://127.0.0.1:1719";
const DistDir = "dist-dev";
let waveSrvReadyResolve = (value: boolean) => {};
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
waveSrvReadyResolve = resolve;
});
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
const unamePlatform = process.platform;
let unameArch: string = process.arch;
if (unameArch == "x64") {
unameArch = "amd64";
}
function getBaseHostPort(): string {
if (isDev) {
return DevServerEndpoint;
}
return ProdServerEndpoint;
}
// must match golang
function getWaveHomeDir() {
return path.join(process.env.HOME, ".w2");
}
function getElectronAppBasePath(): string {
return path.dirname(__dirname);
}
function getGoAppBasePath(): string {
const appDir = getElectronAppBasePath();
if (appDir.endsWith(".asar")) {
return `${appDir}.unpacked`;
} else {
return appDir;
}
}
function getWaveSrvPath(): string {
return path.join(getGoAppBasePath(), "bin", "wavesrv");
}
function getWaveSrvCmd(): string {
const waveSrvPath = getWaveSrvPath();
const waveHome = getWaveHomeDir();
const logFile = path.join(waveHome, "wavesrv.log");
return `"${waveSrvPath}" >> "${logFile}" 2>&1`;
}
function getWaveSrvCwd(): string {
return getWaveHomeDir();
}
function runWaveSrv(): Promise<boolean> {
let pResolve: (value: boolean) => void;
let pReject: (reason?: any) => void;
const rtnPromise = new Promise<boolean>((argResolve, argReject) => {
pResolve = argResolve;
pReject = argReject;
});
const envCopy = { ...process.env };
envCopy[WaveAppPathVarName] = getGoAppBasePath();
if (isDev) {
envCopy[WaveDevVarName] = "1";
}
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
const waveSrvCmd = getWaveSrvCmd();
console.log("trying to run local server", waveSrvCmd);
const proc = child_process.execFile("bash", ["-c", waveSrvCmd], {
cwd: getWaveSrvCwd(),
env: envCopy,
});
proc.on("exit", (e) => {
console.log("wavesrv exited, shutting down");
electronApp.quit();
});
proc.on("spawn", (e) => {
console.log("spawnned wavesrv");
waveSrvProc = proc;
pResolve(true);
});
proc.on("error", (e) => {
console.log("error running wavesrv", e);
pReject(e);
});
proc.stdout.on("data", (_) => {
return;
});
proc.stderr.on("data", (_) => {
return;
});
return rtnPromise;
}
function mainResizeHandler(_: any, win: Electron.BrowserWindow) {
if (win == null || win.isDestroyed() || win.fullScreen) {
return;
}
const bounds = win.getBounds();
const winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x };
const url = new URL(getBaseHostPort() + "/api/set-winsize");
// TODO
}
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
event.preventDefault();
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
console.log("open external, shNav", url);
electron.shell.openExternal(url);
} else {
console.log("navigation canceled", url);
}
}
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
if (!event.frame?.parent) {
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
return;
}
const url = event.url;
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
if (event.frame.name == "webview") {
// "webview" links always open in new window
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
console.log("open external, frameNav", url);
event.preventDefault();
electron.shell.openExternal(url);
return;
}
if (event.frame.name == "pdfview" && url.startsWith("blob:file:///")) {
// allowed
return;
}
event.preventDefault();
console.log("frame navigation canceled");
}
function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserWindow {
const win = new electron.BrowserWindow({
x: 200,
y: 200,
titleBarStyle: "hiddenInset",
width: waveWindow.winsize.width,
height: waveWindow.winsize.height,
minWidth: 500,
minHeight: 300,
icon:
unamePlatform == "linux"
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
: undefined,
webPreferences: {
preload: path.join(getElectronAppBasePath(), DistDir, "preload.js"),
},
show: false,
autoHideMenuBar: true,
backgroundColor: "#000000",
});
win.once("ready-to-show", () => {
win.show();
});
// const indexHtml = isDev ? "index-dev.html" : "index.html";
let usp = new URLSearchParams();
usp.set("clientid", client.oid);
usp.set("windowid", waveWindow.oid);
const indexHtml = "index.html";
win.loadFile(path.join(getElectronAppBasePath(), "public", indexHtml), { search: usp.toString() });
win.webContents.on("will-navigate", shNavHandler);
win.webContents.on("will-frame-navigate", shFrameNavHandler);
win.on(
"resize",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.on(
"move",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed");
});
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
console.log("openExternal fallback", url);
electron.shell.openExternal(url);
}
console.log("window-open denied", url);
return { action: "deny" };
});
return win;
}
process.on("SIGUSR1", function () {
waveSrvReadyResolve(true);
});
(async () => {
const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock();
if (!instanceLock) {
console.log("waveterm-app could not get single-instance-lock, shutting down");
electronApp.quit();
return;
}
try {
await runWaveSrv();
} catch (e) {
console.log(e.toString());
}
console.log("waiting for wavesrv ready signal (SIGUSR1)");
const ready = await waveSrvReady;
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
let clientData = await services.ClientService.GetClientData();
let windowData: WaveWindow = (await services.ObjectService.GetObject(
"window:" + clientData.mainwindowid
)) as WaveWindow;
await electronApp.whenReady();
await createWindow(clientData, windowData);
electronApp.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
createWindow(clientData, windowData);
}
});
})();
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
let { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {});
+15 -1
View File
@@ -4,4 +4,18 @@ import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier);
const baseConfig = tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier);
const customConfig = {
...baseConfig,
overrides: [
{
files: ["emain/emain.ts", "vite.config.ts", "electron.vite.config.ts"],
env: {
node: true,
},
},
],
};
export default customConfig;
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
@import "./reset.less";
@import "./theme.less";
body {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
background-color: var(--main-bg-color);
color: var(--main-text-color);
font: var(--base-font);
overflow: hidden;
}
*::-webkit-scrollbar {
width: 4px;
height: 4px;
}
*::-webkit-scrollbar-track {
background-color: var(--scrollbar-background-color) !important;
}
*::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb-color) !important;
border-radius: 4px;
margin: 0 1px 0 1px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover-color) !important;
}
.flex-spacer {
flex-grow: 1;
}
.text-fixed {
font: var(--fixed-font);
}
#main,
.mainapp {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.titlebar {
height: 35px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
-webkit-app-region: drag;
}
.error-boundary {
color: var(--error-color);
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { Provider } from "jotai";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "../../public/style.less";
import "./app.less";
import { CenteredDiv } from "./element/quickelems";
const App = () => {
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import React from "react";
import "./quickelems.less";
function CenteredLoadingDiv() {
+60 -26
View File
@@ -1,15 +1,22 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Events } from "@wailsio/runtime";
import * as jotai from "jotai";
import * as rxjs from "rxjs";
import * as WOS from "./wos";
import { WSControl } from "./ws";
// TODO remove the window dependency completely
// we should have the initialization be more orderly -- proceed directly from wave.ts instead of on its own.
const globalStore = jotai.createStore();
const urlParams = new URLSearchParams(window.location.search);
const globalWindowId = urlParams.get("windowid");
const globalClientId = urlParams.get("clientid");
let globalWindowId: string = null;
let globalClientId: string = null;
if (typeof window !== "undefined") {
// this if statement allows us to use the code in nodejs as well
const urlParams = new URLSearchParams(window.location.search);
globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342";
globalClientId = urlParams.get("clientid") || "f4bc1713-a364-41b3-a5c4-b000ba10d622";
}
const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
globalStore.set(windowIdAtom, globalWindowId);
@@ -18,7 +25,7 @@ const uiContextAtom = jotai.atom((get) => {
const windowData = get(windowDataAtom);
const uiContext: UIContext = {
windowid: get(atoms.windowId),
activetabid: windowData.activetabid,
activetabid: windowData?.activetabid,
};
return uiContext;
}) as jotai.Atom<UIContext>;
@@ -34,7 +41,8 @@ const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
if (windowId == null) {
return null;
}
return WOS.getObjectValue(WOS.makeORef("window", windowId), get);
const rtn = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", windowId), get);
return rtn;
});
const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
@@ -56,10 +64,10 @@ const atoms = {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>();
const orefSubjects = new Map<string, SubjectWithRef<any>>();
function getBlockSubject(blockId: string): SubjectWithRef<any> {
let subject = blockSubjects.get(blockId);
function getORefSubject(oref: string): SubjectWithRef<any> {
let subject = orefSubjects.get(oref);
if (subject == null) {
subject = new rxjs.Subject<any>() as any;
subject.refCount = 0;
@@ -67,29 +75,15 @@ function getBlockSubject(blockId: string): SubjectWithRef<any> {
subject.refCount--;
if (subject.refCount === 0) {
subject.complete();
blockSubjects.delete(blockId);
orefSubjects.delete(oref);
}
};
blockSubjects.set(blockId, subject);
orefSubjects.set(oref, subject);
}
subject.refCount++;
return subject;
}
Events.On("block:ptydata", (event: any) => {
const data = event?.data;
if (data?.blockid == null) {
console.log("block:ptydata with null blockid");
return;
}
// we don't use getBlockSubject here because we don't want to create a new subject
const subject = blockSubjects.get(data.blockid);
if (subject == null) {
return;
}
subject.next(data);
});
const blockCache = new Map<string, Map<string, any>>();
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
@@ -123,4 +117,44 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>;
}
export { WOS, atoms, getBlockSubject, globalStore, useBlockAtom, useBlockCache };
function getBackendHostPort(): string {
// TODO deal with dev/production
return "http://localhost:8190";
}
function getBackendWSHostPort(): string {
return "ws://localhost:8191";
}
let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) {
if (msg.oref == null) {
console.log("unsupported event", msg);
return;
}
// we don't use getORefSubject here because we don't want to create a new subject
const subject = orefSubjects.get(msg.oref);
if (subject == null) {
return;
}
subject.next(msg.data);
}
function handleWSMessage(msg: any) {
if (msg == null) {
return;
}
if (msg.eventtype != null) {
handleWSEventMessage(msg);
}
}
function initWS() {
globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => {
handleWSMessage(msg);
});
globalWS.connectNow("initWS");
}
export { WOS, atoms, getBackendHostPort, getORefSubject, globalStore, globalWS, initWS, useBlockAtom, useBlockCache };
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// generated by cmd/generate/main-generate.go
import * as WOS from "./wos";
// blockservice.BlockService (block)
class BlockServiceType {
// send command to block
SendCommand(blockid: string, command: MetaType): Promise<void> {
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
}
}
export const BlockService = new BlockServiceType()
// clientservice.ClientService (client)
class ClientServiceType {
GetClientData(): Promise<Client> {
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
}
GetTab(arg1: string): Promise<Tab> {
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
}
GetWindow(arg1: string): Promise<Window> {
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
}
GetWorkspace(arg1: string): Promise<Workspace> {
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
}
}
export const ClientService = new ClientServiceType()
// fileservice.FileService (file)
class FileServiceType {
GetWaveFile(arg1: string, arg2: string): Promise<any> {
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
}
ReadFile(arg1: string): Promise<FullFile> {
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
}
StatFile(arg1: string): Promise<FileInfo> {
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
}
}
export const FileService = new FileServiceType()
// objectservice.ObjectService (object)
class ObjectServiceType {
// @returns tabId (and object updates)
AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<string> {
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
}
// @returns object updates
CloseTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
}
// @returns blockId (and object updates)
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
}
// @returns object updates
DeleteBlock(blockId: string): Promise<void> {
return WOS.callBackendService("object", "DeleteBlock", Array.from(arguments))
}
// get wave object by oref
GetObject(oref: string): Promise<WaveObj> {
return WOS.callBackendService("object", "GetObject", Array.from(arguments))
}
// @returns objects
GetObjects(orefs: string[]): Promise<WaveObj[]> {
return WOS.callBackendService("object", "GetObjects", Array.from(arguments))
}
// @returns object updates
SetActiveTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments))
}
// @returns object updates
UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<void> {
return WOS.callBackendService("object", "UpdateObject", Array.from(arguments))
}
// @returns object updates
UpdateObjectMeta(oref: string, meta: MetaType): Promise<void> {
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
}
}
export const ObjectService = new ObjectServiceType()
+74 -68
View File
@@ -3,10 +3,13 @@
// WaveObjectStore
import { Call as $Call, Events } from "@wailsio/runtime";
// import { Call as $Call, Events } from "@wailsio/runtime";
import * as jotai from "jotai";
import * as React from "react";
import { atoms, globalStore } from "./global";
import { atoms, getBackendHostPort, globalStore } from "./global";
import * as services from "./services";
const IsElectron = true;
type WaveObjectDataItemType<T extends WaveObj> = {
value: T;
@@ -54,7 +57,51 @@ function makeORef(otype: string, oid: string): string {
}
function GetObject<T>(oref: string): Promise<T> {
return $Call.ByName("github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", oref);
return callBackendService("object", "GetObject", [oref], true);
}
function callBackendService(service: string, method: string, args: any[], noUIContext?: boolean): Promise<any> {
const startTs = Date.now();
let uiContext: UIContext = null;
if (!noUIContext) {
uiContext = globalStore.get(atoms.uiContext);
}
let waveCall: WebCallType = {
service: service,
method: method,
args: args,
uicontext: uiContext,
};
// usp is just for debugging (easier to filter URLs)
let methodName = service + "." + method;
let usp = new URLSearchParams();
usp.set("service", service);
usp.set("method", method);
let fetchPromise = fetch(getBackendHostPort() + "/wave/service?" + usp.toString(), {
method: "POST",
body: JSON.stringify(waveCall),
});
let prtn = fetchPromise
.then((resp) => {
if (!resp.ok) {
throw new Error(`call ${methodName} failed: ${resp.status} ${resp.statusText}`);
}
return resp.json();
})
.then((respData: WebReturnType) => {
if (respData == null) {
return null;
}
if (respData.updates != null) {
updateWaveObjects(respData.updates);
}
if (respData.error != null) {
throw new Error(`call ${methodName} error: ${respData.error}`);
}
console.log("Call", methodName, Date.now() - startTs + "ms");
return respData.data;
});
return prtn;
}
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
@@ -75,6 +122,7 @@ function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boo
const localPromise = GetObject<T>(oref);
wov.pendingPromise = localPromise;
localPromise.then((val) => {
console.log("GetObject resolved", oref, val);
if (wov.pendingPromise != localPromise) {
return;
}
@@ -187,7 +235,7 @@ function useWaveObject<T extends WaveObj>(oref: string): [T, boolean, (val: T) =
const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
const simpleSet = (val: T) => {
setAtomVal({ value: val, loading: false });
UpdateObject(val, false);
services.ObjectService.UpdateObject(val, false);
};
return [atomVal.value, atomVal.loading, simpleSet];
}
@@ -236,48 +284,32 @@ function cleanWaveObjectCache() {
}
}
Events.On("waveobj:update", (event: any) => {
const data: WaveObjUpdate[] = event?.data;
if (data == null) {
return;
}
if (!Array.isArray(data)) {
console.log("invalid waveobj:update, not an array", data);
return;
}
if (data.length == 0) {
return;
}
updateWaveObjects(data);
});
function wrapObjectServiceCall<T>(fnName: string, ...args: any[]): Promise<T> {
const uiContext = globalStore.get(atoms.uiContext);
const startTs = Date.now();
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService." + fnName,
uiContext,
...args
);
prtn = prtn.then((val) => {
console.log("Call", fnName, Date.now() - startTs + "ms");
if (val.updates) {
updateWaveObjects(val.updates);
}
return val;
});
return prtn;
}
// Events.On("waveobj:update", (event: any) => {
// const data: WaveObjUpdate[] = event?.data;
// if (data == null) {
// return;
// }
// if (!Array.isArray(data)) {
// console.log("invalid waveobj:update, not an array", data);
// return;
// }
// if (data.length == 0) {
// return;
// }
// updateWaveObjects(data);
// });
// gets the value of a WaveObject from the cache.
// should provide getFn if it is available (e.g. inside of a jotai atom)
// otherwise it will use the globalStore.get function
function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T {
const wov = waveObjectValueCache.get(oref);
if (wov === undefined) {
return null;
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
console.log("wov is null, creating new wov", oref);
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
}
if (getFn === undefined) {
if (getFn == null) {
getFn = globalStore.get;
}
const atomVal = getFn(wov.dataAtom);
@@ -298,38 +330,12 @@ function setObjectValue<T extends WaveObj>(value: T, setFn?: jotai.Setter, pushT
}
setFn(wov.dataAtom, { value: value, loading: false });
if (pushToServer) {
UpdateObject(value, false);
services.ObjectService.UpdateObject(value, false);
}
}
export function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> {
return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab);
}
export function SetActiveTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("SetActiveTab", tabId);
}
export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> {
return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts);
}
export function DeleteBlock(blockId: string): Promise<void> {
return wrapObjectServiceCall("DeleteBlock", blockId);
}
export function CloseTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("CloseTab", tabId);
}
export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise<void> {
return wrapObjectServiceCall("UpdateObjectMeta", blockId, meta);
}
export function UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<WaveObjUpdate[]> {
return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates);
}
export {
callBackendService,
cleanWaveObjectCache,
clearWaveObjectCache,
getObjectValue,
+254
View File
@@ -0,0 +1,254 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as jotai from "jotai";
import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
const MaxWebSocketSendSize = 1024 * 1024; // 1MB
type RpcEntry = {
reqId: string;
startTs: number;
method: string;
resolve: (any) => void;
reject: (any) => void;
promise: Promise<any>;
};
type JotaiStore = {
get: <Value>(atom: jotai.Atom<Value>) => Value;
set: <Value>(atom: jotai.WritableAtom<Value, [Value], void>, value: Value) => void;
};
class WSControl {
wsConn: any;
open: jotai.WritableAtom<boolean, [boolean], void>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
windowId: string;
messageCallback: (any) => void = null;
watchSessionId: string = null;
watchScreenId: string = null;
wsLog: string[] = [];
authKey: string;
baseHostPort: string;
lastReconnectTime: number = 0;
rpcMap: Map<string, RpcEntry> = new Map(); // reqId -> RpcEntry
jotaiStore: JotaiStore;
constructor(
baseHostPort: string,
jotaiStore: JotaiStore,
windowId: string,
authKey: string,
messageCallback: (any) => void
) {
this.baseHostPort = baseHostPort;
this.messageCallback = messageCallback;
this.windowId = windowId;
this.authKey = authKey;
this.open = jotai.atom(false);
this.jotaiStore = jotaiStore;
setInterval(this.sendPing.bind(this), 5000);
}
log(str: string) {
let ts = Date.now();
this.wsLog.push("[" + ts + "] " + str);
if (this.wsLog.length > 50) {
this.wsLog.splice(0, this.wsLog.length - 50);
}
}
setOpen(val: boolean) {
this.jotaiStore.set(this.open, val);
}
isOpen() {
return this.jotaiStore.get(this.open);
}
connectNow(desc: string) {
if (this.isOpen()) {
return;
}
this.lastReconnectTime = Date.now();
this.log(sprintf("try reconnect (%s)", desc));
this.opening = true;
this.wsConn = new WebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId);
this.wsConn.onopen = this.onopen.bind(this);
this.wsConn.onmessage = this.onmessage.bind(this);
this.wsConn.onclose = this.onclose.bind(this);
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose?: boolean) {
if (this.isOpen()) {
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
}
return;
}
this.reconnectTimes++;
if (this.reconnectTimes > 20) {
this.log("cannot connect, giving up");
return;
}
let timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60];
let timeout = 60;
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
if (Date.now() - this.lastReconnectTime < 500) {
timeout = 1;
}
if (timeout > 0) {
this.log(sprintf("sleeping %ds", timeout));
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout * 1000);
}
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
} else {
this.log("connection error/disconnected");
}
if (this.isOpen() || this.opening) {
this.setOpen(false);
this.opening = false;
this.reconnect();
}
}
onopen() {
this.log("connection open");
this.setOpen(true);
this.opening = false;
this.runMsgQueue();
// reconnectTimes is reset in onmessage:hello
}
runMsgQueue() {
if (!this.isOpen()) {
return;
}
if (this.msgQueue.length == 0) {
return;
}
let msg = this.msgQueue.shift();
this.sendMessage(msg);
setTimeout(() => {
this.runMsgQueue();
}, 100);
}
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
}
if (eventData == null) {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
// nothing
return;
}
if (eventData.type == "hello") {
this.reconnectTimes = 0;
return;
}
if (eventData.type == "rpcresp") {
this.handleRpcResp(eventData);
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
} catch (e) {
console.log("[error] messageCallback", e);
}
}
}
sendPing() {
if (!this.isOpen()) {
return;
}
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
handleRpcResp(data: any) {
let reqId = data.reqid;
let rpcEntry = this.rpcMap.get(reqId);
if (rpcEntry == null) {
console.log("rpcresp for unknown reqid", reqId);
return;
}
this.rpcMap.delete(reqId);
console.log("rpcresp", rpcEntry.method, Math.round(performance.now() - rpcEntry.startTs) + "ms");
if (data.error != null) {
rpcEntry.reject(data.error);
} else {
rpcEntry.resolve(data.data);
}
}
doRpc(method: string, params: any[]): Promise<any> {
if (!this.isOpen()) {
return Promise.reject("not connected");
}
let reqId = uuidv4();
let req = { type: "rpc", method: method, params: params, reqid: reqId };
let rpcEntry: RpcEntry = {
method: method,
startTs: performance.now(),
reqId: reqId,
resolve: null,
reject: null,
promise: null,
};
let rpcPromise = new Promise((resolve, reject) => {
rpcEntry.resolve = resolve;
rpcEntry.reject = reject;
});
rpcEntry.promise = rpcPromise;
this.rpcMap.set(reqId, rpcEntry);
this.wsConn.send(JSON.stringify(req));
return rpcPromise;
}
sendMessage(data: any) {
if (!this.isOpen()) {
return;
}
let msg = JSON.stringify(data);
const byteSize = new Blob([msg]).size;
if (byteSize > MaxWebSocketSendSize) {
console.log("ws message too large", byteSize, data.type, msg.substring(0, 100));
return;
}
this.wsConn.send(msg);
}
pushMessage(data: any) {
if (!this.isOpen()) {
this.msgQueue.push(data);
return;
}
this.sendMessage(data);
}
}
export { WSControl };
+3 -2
View File
@@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import { Block, BlockHeader } from "@/app/block/block";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react";
import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems";
import "./tab.less";
const TabContent = ({ tabId }: { tabId: string }) => {
@@ -34,7 +35,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
const onNodeDelete = useCallback((data: TabLayoutData) => {
console.log("onNodeDelete", data);
return WOS.DeleteBlock(data.blockId);
return services.ObjectService.DeleteBlock(data.blockId);
}, []);
if (tabLoading) {
+1 -1
View File
@@ -15,7 +15,7 @@ declare var monaco: Monaco;
let monacoLoadedAtom = jotai.atom(false);
function loadMonaco() {
loader.config({ paths: { vs: "./monaco" } });
loader.config({ paths: { vs: "./dist-dev/monaco" } });
loader
.init()
.then(() => {

Some files were not shown because too many files have changed in this diff Show More