mirror of
https://github.com/cf-sonr/motr.git
synced 2026-01-12 11:09:13 +00:00
feat: enable WASM execution environment
This commit is contained in:
@@ -3,6 +3,7 @@ version: 2
|
||||
project_name: motr
|
||||
builds:
|
||||
- id: motr
|
||||
main: ./cmd/vault
|
||||
binary: app
|
||||
goos:
|
||||
- js
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# https://taskfile.dev
|
||||
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||
|
||||
version: "3"
|
||||
silent: true
|
||||
|
||||
vars:
|
||||
ROOT_DIR:
|
||||
sh: git rev-parse --show-toplevel
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
@@ -37,6 +41,7 @@ tasks:
|
||||
|
||||
build:go:
|
||||
desc: Builds Go code
|
||||
dir: "{{.ROOT_DIR}}"
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
@@ -46,10 +51,11 @@ tasks:
|
||||
generates:
|
||||
- bin/motr.wasm
|
||||
cmds:
|
||||
- go build -o bin/motr.wasm .
|
||||
- go build -o bin/motr.wasm ./cmd/vault
|
||||
|
||||
build:docker:
|
||||
desc: Builds Docker image
|
||||
dir: "{{.ROOT_DIR}}"
|
||||
env:
|
||||
GOOS: js
|
||||
GOARCH: wasm
|
||||
@@ -63,6 +69,7 @@ tasks:
|
||||
|
||||
gen:templ:
|
||||
internal: true
|
||||
dir: "{{.ROOT_DIR}}"
|
||||
sources:
|
||||
- "**/*.templ"
|
||||
generates:
|
||||
@@ -72,19 +79,21 @@ tasks:
|
||||
|
||||
gen:sqlc:
|
||||
internal: true
|
||||
dir: "{{.ROOT_DIR}}"
|
||||
sources:
|
||||
- pkg/sink/query.sql
|
||||
- pkg/sink/schema.sql
|
||||
- "**/query.sql"
|
||||
- "**/schema.sql"
|
||||
generates:
|
||||
- pkg/models/db.go
|
||||
- pkg/models/querier.go
|
||||
- pkg/models/models.go
|
||||
- pkg/models/query.sql.go
|
||||
- "**/db.go"
|
||||
- "**/querier.go"
|
||||
- "**/models.go"
|
||||
- "**/query.sql.go"
|
||||
cmds:
|
||||
- sqlc generate
|
||||
|
||||
test:go:
|
||||
internal: true
|
||||
dir: "{{.ROOT_DIR}}"
|
||||
sources:
|
||||
- "**/*.go"
|
||||
cmds:
|
||||
4
Makefile
4
Makefile
@@ -7,7 +7,7 @@ COMMIT := $(shell git log -1 --format='%H')
|
||||
all: generate build
|
||||
|
||||
build:
|
||||
@task -t .taskfile.dist.yml build
|
||||
@task -t .taskfiles/Default.yml build
|
||||
|
||||
generate:
|
||||
@task -t .taskfile.dist.yml gen
|
||||
@task -t .taskfiles/Default.yml gen
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package context
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/onsonr/motr/internal/models"
|
||||
sink "github.com/onsonr/motr/internal/sink"
|
||||
vault "github.com/onsonr/motr/server"
|
||||
"github.com/onsonr/motr/server"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -47,7 +47,7 @@ func main() {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
e, err := vault.New(nil, dbq)
|
||||
e, err := server.New(nil, dbq, WASMMiddleware)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
43
cmd/vault/context.go
Normal file
43
cmd/vault/context.go
Normal file
@@ -0,0 +1,43 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func WASMMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Extract WASM context from headers
|
||||
if wasmCtx := c.Request().Header.Get("X-Wasm-Context"); wasmCtx != "" {
|
||||
if ctx, err := decodeWasmContext(wasmCtx); err == nil {
|
||||
c.Set("wasm_context", ctx)
|
||||
}
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// // loadConfig loads the config from the given JSON string
|
||||
//
|
||||
// func loadConfig(configString string) (*motr.Config, error) {
|
||||
// var config motr.Config
|
||||
// err := json.Unmarshal([]byte(configString), &config)
|
||||
// return &config, err
|
||||
// }
|
||||
//
|
||||
|
||||
// decodeWasmContext decodes the WASM context from a base64 encoded string
|
||||
func decodeWasmContext(ctx string) (map[string]any, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ctxData map[string]any
|
||||
err = json.Unmarshal(decoded, &ctxData)
|
||||
return ctxData, err
|
||||
}
|
||||
28
cmd/vault/database.go
Normal file
28
cmd/vault/database.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/onsonr/motr/internal/models"
|
||||
sink "github.com/onsonr/motr/internal/sink"
|
||||
)
|
||||
|
||||
// createDB initializes and returns a configured database connection
|
||||
func createDB() (*models.Queries, error) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create tables
|
||||
if _, err := db.ExecContext(context.Background(), sink.SchemaVaultSQL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.New(db), nil
|
||||
}
|
||||
47
cmd/vault/main.go
Normal file
47
cmd/vault/main.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/onsonr/motr/server"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global buffer pool to reduce allocations
|
||||
bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
// Cached JS globals
|
||||
jsGlobal = js.Global()
|
||||
jsUint8Array = jsGlobal.Get("Uint8Array")
|
||||
jsResponse = jsGlobal.Get("Response")
|
||||
jsPromise = jsGlobal.Get("Promise")
|
||||
jsWasmHTTP = jsGlobal.Get("wasmhttp")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// configString := "TODO"
|
||||
// config, _ := loadConfig(configString)
|
||||
dbq, err := createDB()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
e, err := server.New(nil, dbq, WASMMiddleware)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
serveFetch(e)
|
||||
}
|
||||
194
cmd/vault/serve.go
Normal file
194
cmd/vault/serve.go
Normal file
@@ -0,0 +1,194 @@
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
// serveFetch serves HTTP requests with optimized handler management
|
||||
func serveFetch(handler http.Handler) func() {
|
||||
h := handler
|
||||
if h == nil {
|
||||
h = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// Optimize prefix handling
|
||||
prefix := strings.TrimRight(jsWasmHTTP.Get("path").String(), "/")
|
||||
if prefix != "" {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(prefix+"/", http.StripPrefix(prefix, h))
|
||||
h = mux
|
||||
}
|
||||
|
||||
// Create request handler function
|
||||
cb := js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
promise, resolve, reject := newPromiseOptimized()
|
||||
|
||||
go handleRequest(h, args[1], resolve, reject)
|
||||
|
||||
return promise
|
||||
})
|
||||
|
||||
jsWasmHTTP.Call("setHandler", cb)
|
||||
return cb.Release
|
||||
}
|
||||
|
||||
// handleRequest processes the request with panic recovery
|
||||
func handleRequest(h http.Handler, jsReq js.Value, resolve, reject func(interface{})) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var errMsg string
|
||||
if err, ok := r.(error); ok {
|
||||
errMsg = fmt.Sprintf("wasmhttp: panic: %+v", err)
|
||||
} else {
|
||||
errMsg = fmt.Sprintf("wasmhttp: panic: %v", r)
|
||||
}
|
||||
reject(errMsg)
|
||||
}
|
||||
}()
|
||||
|
||||
recorder := newResponseRecorder()
|
||||
h.ServeHTTP(recorder, buildRequest(jsReq))
|
||||
resolve(recorder.jsResponse())
|
||||
}
|
||||
|
||||
// buildRequest creates an http.Request from JS Request
|
||||
func buildRequest(jsReq js.Value) *http.Request {
|
||||
// Get request body
|
||||
arrayBuffer, err := awaitPromiseOptimized(jsReq.Call("arrayBuffer"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create body buffer
|
||||
jsBody := jsUint8Array.New(arrayBuffer)
|
||||
bodyLen := jsBody.Get("length").Int()
|
||||
body := make([]byte, bodyLen)
|
||||
js.CopyBytesToGo(body, jsBody)
|
||||
|
||||
// Create request
|
||||
req := httptest.NewRequest(
|
||||
jsReq.Get("method").String(),
|
||||
jsReq.Get("url").String(),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
|
||||
// Set headers efficiently
|
||||
headers := jsReq.Get("headers")
|
||||
headersIt := headers.Call("entries")
|
||||
for {
|
||||
entry := headersIt.Call("next")
|
||||
if entry.Get("done").Bool() {
|
||||
break
|
||||
}
|
||||
pair := entry.Get("value")
|
||||
req.Header.Set(pair.Index(0).String(), pair.Index(1).String())
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// ResponseRecorder with optimized buffer handling
|
||||
type ResponseRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func newResponseRecorder() *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
buffer: bufferPool.Get().(*bytes.Buffer),
|
||||
}
|
||||
}
|
||||
|
||||
// jsResponse creates a JS Response with optimized memory usage
|
||||
func (rr *ResponseRecorder) jsResponse() js.Value {
|
||||
defer func() {
|
||||
rr.buffer.Reset()
|
||||
bufferPool.Put(rr.buffer)
|
||||
}()
|
||||
|
||||
res := rr.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
// Prepare response body
|
||||
body := js.Undefined()
|
||||
if res.ContentLength != 0 {
|
||||
if _, err := io.Copy(rr.buffer, res.Body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bodyBytes := rr.buffer.Bytes()
|
||||
body = jsUint8Array.New(len(bodyBytes))
|
||||
js.CopyBytesToJS(body, bodyBytes)
|
||||
}
|
||||
|
||||
// Prepare response init object
|
||||
init := make(map[string]interface{}, 3)
|
||||
if res.StatusCode != 0 {
|
||||
init["status"] = res.StatusCode
|
||||
}
|
||||
|
||||
if len(res.Header) > 0 {
|
||||
headers := make(map[string]interface{}, len(res.Header))
|
||||
for k, v := range res.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
init["headers"] = headers
|
||||
}
|
||||
|
||||
return jsResponse.New(body, init)
|
||||
}
|
||||
|
||||
// newPromiseOptimized creates a new JavaScript Promise with optimized callback handling
|
||||
func newPromiseOptimized() (js.Value, func(interface{}), func(interface{})) {
|
||||
var (
|
||||
resolve func(interface{})
|
||||
reject func(interface{})
|
||||
promiseFunc = js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
resolve = func(v interface{}) { args[0].Invoke(v) }
|
||||
reject = func(v interface{}) { args[1].Invoke(v) }
|
||||
return js.Undefined()
|
||||
})
|
||||
)
|
||||
defer promiseFunc.Release()
|
||||
|
||||
return jsPromise.New(promiseFunc), resolve, reject
|
||||
}
|
||||
|
||||
// awaitPromiseOptimized waits for Promise resolution with optimized channel handling
|
||||
func awaitPromiseOptimized(promise js.Value) (js.Value, error) {
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
result js.Value
|
||||
err error
|
||||
)
|
||||
|
||||
thenFunc := js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
result = args[0]
|
||||
close(done)
|
||||
return nil
|
||||
})
|
||||
defer thenFunc.Release()
|
||||
|
||||
catchFunc := js.FuncOf(func(_ js.Value, args []js.Value) interface{} {
|
||||
err = js.Error{Value: args[0]}
|
||||
close(done)
|
||||
return nil
|
||||
})
|
||||
defer catchFunc.Release()
|
||||
|
||||
promise.Call("then", thenFunc).Call("catch", catchFunc)
|
||||
<-done
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package handlers
|
||||
@@ -1 +0,0 @@
|
||||
package handlers
|
||||
@@ -5,15 +5,14 @@ package server
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
echomiddleware "github.com/labstack/echo/v4/middleware"
|
||||
"github.com/onsonr/motr/internal/context"
|
||||
"github.com/onsonr/motr/internal/models"
|
||||
"github.com/onsonr/motr/types"
|
||||
"github.com/onsonr/motr/internal/types"
|
||||
)
|
||||
|
||||
type Vault = *echo.Echo
|
||||
|
||||
// New returns a new Vault instance
|
||||
func New(config *types.Config, dbq *models.Queries) (Vault, error) {
|
||||
func New(config *types.Config, dbq *models.Queries, mdws ...echo.MiddlewareFunc) (Vault, error) {
|
||||
e := echo.New()
|
||||
// Override default behaviors
|
||||
e.IPExtractor = echo.ExtractIPDirect()
|
||||
@@ -22,7 +21,7 @@ func New(config *types.Config, dbq *models.Queries) (Vault, error) {
|
||||
// Built-in middleware
|
||||
e.Use(echomiddleware.Logger())
|
||||
e.Use(echomiddleware.Recover())
|
||||
e.Use(context.WASMMiddleware)
|
||||
e.Use(mdws...)
|
||||
registerRoutes(e)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user