mess around with support WASM builds

This commit is contained in:
Joel Wetzell
2026-03-30 14:46:35 -05:00
parent 882af2948a
commit 050ada6a70
31 changed files with 487 additions and 6 deletions

5
app/demo/document.go Normal file
View File

@@ -0,0 +1,5 @@
package main
import "syscall/js"
var document = js.Global().Get("document")

74
app/demo/index.html Normal file
View File

@@ -0,0 +1,74 @@
<html>
<head>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
padding: 0;
}
#editor-container {
flex-grow: 1;
}
#log-container {
background-color: #1e1e1e;
width: 100%;
height: 200px;
overflow-y: auto;
overflow-x: scroll;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
}
#logs {
margin: 0;
padding-left: 2px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #a8cc8c;
}
</style>
<script>
const container = document.getElementById("log-container");
function isScrolledToBottom() {
const scrollThreshold = 1;
return (
container.scrollHeight - container.clientHeight <=
container.scrollTop + scrollThreshold
);
}
function scrollToBottomManual() {
const container = document.getElementById("log-container");
// Set scrollTop to the maximum value possible (scrollHeight - clientHeight)
container.scrollTop = container.scrollHeight - container.clientHeight;
}
</script>
</head>
<body>
<div id="editor-container">
<button id="button1">Button 1</button>
<button id="button2">Button 2</button>
<div id="output1"></div>
<div id="output2"></div>
</div>
<div id="log-container">
<pre id="logs"></pre>
</div>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch("main.wasm"),
go.importObject,
).then((result) => {
go.run(result.instance);
});
</script>
</body>
</html>

42
app/demo/log.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import "syscall/js"
type LogWriter struct {
Element js.Value
Container js.Value
}
func (pw *LogWriter) ScrollToBottom() {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
pw.Container.Set("scrollTop", scrollHeight-clientHeight)
}
func (pw *LogWriter) IsScrolledToBottom() bool {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
scrollTop := pw.Container.Get("scrollTop").Int()
return scrollHeight-clientHeight <= scrollTop+25
}
func (pw *LogWriter) Write(p []byte) (n int, err error) {
if !pw.Element.IsUndefined() {
currentText := pw.Element.Get("textContent").String()
newText := currentText + string(p)
pw.Element.Set("textContent", newText)
if pw.IsScrolledToBottom() {
pw.ScrollToBottom()
}
}
return len(p), nil
}
func NewLogWriter(id string) *LogWriter {
element := document.Call("getElementById", id)
container := element.Get("parentElement")
return &LogWriter{
Element: element,
Container: container,
}
}

121
app/demo/main.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"context"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.SetDefault(slog.New(slog.NewTextHandler(NewLogWriter("logs"), &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
router, moduleConfigErrors, routeConfigErrors := showbridge.NewRouter(config.Config{
Api: config.ApiConfig{
Enabled: false,
Port: 0,
},
Modules: []config.ModuleConfig{
{
Id: "timer",
Type: "time.interval",
Params: map[string]any{
"duration": 1000,
},
},
{
Id: "button1",
Type: "web.onclick",
Params: map[string]any{
"id": "button1",
},
},
{
Id: "button2",
Type: "web.onclick",
Params: map[string]any{
"id": "button2",
},
},
},
Routes: []config.RouteConfig{
{
Input: "timer",
Processors: []config.ProcessorConfig{
{
Type: "debug.log",
},
},
},
{
Input: "button1",
Processors: []config.ProcessorConfig{
{
Type: "string.create",
Params: map[string]any{
"template": "{{.Payload.UnixMilli}}",
},
},
{
Type: "debug.log",
},
{
Type: "web.set",
Params: map[string]any{
"id": "output1",
"property": "innerText",
"value": "Button1 Pressed @ {{.Payload}}",
},
},
},
},
{
Input: "button2",
Processors: []config.ProcessorConfig{
{
Type: "string.create",
Params: map[string]any{
"template": "{{.Payload.UnixMilli}}",
},
},
{
Type: "debug.log",
},
{
Type: "web.set",
Params: map[string]any{
"id": "output2",
"property": "innerText",
"value": "Button2 Pressed @ {{.Payload}}",
},
},
},
},
},
})
if len(moduleConfigErrors) > 0 {
for _, err := range moduleConfigErrors {
println("Module config error:", err.Error)
}
}
if len(routeConfigErrors) > 0 {
for _, err := range routeConfigErrors {
println("Route config error:", err.Error)
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
router.Start(ctx)
fmt.Println("router stopped")
}()
<-ctx.Done()
}

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -64,7 +64,7 @@ func (t *TimeTimer) Start(ctx context.Context) error {
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("net.tcp.client unable to get router from context") return errors.New("time.timer unable to get router from context")
} }
t.router = router t.router = router
moduleContext, cancel := context.WithCancel(ctx) moduleContext, cancel := context.WithCancel(ctx)

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package module package module
import ( import (

View File

@@ -0,0 +1,91 @@
//go:build js
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"syscall/js"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type WebOnClick struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
logger *slog.Logger
ElementId string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "web.onclick",
Title: "On Click",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Element ID",
Type: "string",
Description: "ID of the HTML element to attach the click listener to",
},
},
Required: []string{"duration"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("web.onclick id error: %w", err)
}
return &WebOnClick{ElementId: idString, config: config, logger: CreateLogger(config)}, nil
},
})
}
func (woc *WebOnClick) Id() string {
return woc.config.Id
}
func (woc *WebOnClick) Type() string {
return woc.config.Type
}
func (woc *WebOnClick) Start(ctx context.Context) error {
woc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
woc.router = router
woc.ctx = ctx
element := js.Global().Get("document").Call("getElementById", woc.ElementId)
if element.IsNull() || element.IsUndefined() {
return fmt.Errorf("web.onclick unable to find element with id: %s", woc.ElementId)
}
element.Set("onclick", js.FuncOf(func(js.Value, []js.Value) interface{} {
if woc.router != nil {
woc.router.HandleInput(woc.ctx, woc.Id(), time.Now())
}
return nil
}))
<-ctx.Done()
return nil
}
func (woc *WebOnClick) Stop() {
}

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -21,6 +21,11 @@ type HTTPRequestDo struct {
URL *template.Template URL *template.Template
} }
type HTTPResponse struct {
Status int
Body []byte
}
func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload templateData := wrappedPayload

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (
@@ -17,11 +19,6 @@ type HTTPResponseCreate struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
type HTTPResponse struct {
Status int
Body []byte
}
func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload templateData := wrappedPayload

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !js
package processor package processor
import ( import (

View File

@@ -0,0 +1,102 @@
//go:build js
package processor
import (
"bytes"
"context"
"fmt"
"html/template"
"log/slog"
"syscall/js"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type WebSet struct {
config config.ProcessorConfig
ModuleId string
ElementId string
Property string
Value *template.Template
logger *slog.Logger
}
func (kvs *WebSet) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
element := js.Global().Get("document").Call("getElementById", kvs.ElementId)
if element.IsNull() || element.IsUndefined() {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("web.set unable to find element with id: %s", kvs.ElementId)
}
var valueBuffer bytes.Buffer
err := kvs.Value.Execute(&valueBuffer, wrappedPayload)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
element.Set(kvs.Property, valueBuffer.String())
return wrappedPayload, nil
}
func (kvs *WebSet) Type() string {
return kvs.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "web.set",
Title: "Set Web Element Property",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Element ID",
Type: "string",
},
"property": {
Title: "Property",
Type: "string",
},
"value": {
Title: "Value",
Type: "string",
},
},
Required: []string{"id", "property", "value"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("web.set id error: %w", err)
}
propertyString, err := params.GetString("property")
if err != nil {
return nil, fmt.Errorf("web.set property error: %w", err)
}
valueString, err := params.GetString("value")
if err != nil {
return nil, fmt.Errorf("web.set value error: %w", err)
}
valueTemplate, err := template.New("template").Parse(valueString)
if err != nil {
return nil, err
}
return &WebSet{config: config, ElementId: idString, Property: propertyString, Value: valueTemplate, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}