Files
2026-07-24 13:29:16 +00:00

104 lines
3.1 KiB
Go

// Command wasm is the browser entry point for sing-vis. Built with
// `GOOS=js GOARCH=wasm`, it exposes a single JS-callable function,
// `singvisAnalyze`, that runs the exact same internal/engine analysis the old
// HTTP server did — only now it runs entirely in the browser (in a Web Worker).
//
// Under js/wasm, net/http transparently uses the browser Fetch API, so the DoH
// resolver and remote rule-set fetching keep working (subject to CORS).
//
//go:build js && wasm
package main
import (
"context"
"encoding/json"
"fmt"
"syscall/js"
"sing-vis/internal/dnsx"
"sing-vis/internal/engine"
)
// analyzeRequest is the single JSON argument passed from JS. It mirrors the old
// POST /api/analyze body.
type analyzeRequest struct {
Config string `json:"config"`
Inputs []string `json:"inputs"`
RuleSetFiles map[string]engine.RuleSetFile `json:"ruleSetFiles"`
DoHServer string `json:"dohServer"`
Network string `json:"network"` // optional: tcp/udp assumption
AssumeResolved *bool `json:"assumeResolved"` // default true
}
const defaultDoHServer = "https://1.1.1.1/dns-query"
func main() {
js.Global().Set("singvisAnalyze", js.FuncOf(analyze))
// Signal readiness to the host (worker) so it need not poll.
if ready := js.Global().Get("singvisReady"); ready.Type() == js.TypeFunction {
ready.Invoke()
}
select {} // keep the Go runtime alive to service calls
}
// analyze is the JS-facing entry point. It takes one JSON string argument and
// returns a Promise<string> that resolves to the marshaled engine.Result (or
// rejects with an Error). The work runs in a goroutine so the JS event loop is
// never blocked while DoH / rule-set fetches are in flight.
func analyze(_ js.Value, args []js.Value) any {
var input string
if len(args) > 0 && args[0].Type() == js.TypeString {
input = args[0].String()
}
handler := js.FuncOf(func(_ js.Value, promiseArgs []js.Value) any {
resolve := promiseArgs[0]
reject := promiseArgs[1]
go func() {
out, err := runAnalyze(input)
if err != nil {
reject.Invoke(jsError(err))
return
}
resolve.Invoke(out)
}()
return nil
})
return js.Global().Get("Promise").New(handler)
}
func runAnalyze(input string) (string, error) {
var req analyzeRequest
if err := json.Unmarshal([]byte(input), &req); err != nil {
return "", fmt.Errorf("invalid request: %w", err)
}
doh := req.DoHServer
if doh == "" {
doh = defaultDoHServer
}
assumeResolved := true
if req.AssumeResolved != nil {
assumeResolved = *req.AssumeResolved
}
result, err := engine.Analyze(context.Background(), engine.Request{
Config: req.Config,
Inputs: req.Inputs,
RuleSetFiles: req.RuleSetFiles,
Network: req.Network,
AssumeResolved: assumeResolved,
Resolver: dnsx.NewDoHResolver(doh),
})
if err != nil {
return "", err
}
out, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(out), nil
}
func jsError(err error) js.Value {
return js.Global().Get("Error").New(err.Error())
}