Files
mosh-go/cmd/mosh-wasm/main.go
T

153 lines
3.0 KiB
Go

//go:build js && wasm
package main
import (
"encoding/base64"
"fmt"
"sync"
"syscall/js"
"time"
mosh "github.com/unixshells/mosh-go"
)
func main() {
js.Global().Set("moshConnect", js.FuncOf(moshConnect))
select {} // keep alive
}
// moshConnect(url, key) → Promise<MoshSession>
// url: WebTransport URL (e.g., "https://relay.example.com/mosh/user/device")
// key: base64-encoded mosh key
func moshConnect(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return reject("moshConnect requires (url, key)")
}
url := args[0].String()
key := args[1].String()
handler := js.FuncOf(func(this js.Value, pargs []js.Value) interface{} {
resolve := pargs[0]
rejectFn := pargs[1]
go func() {
session, err := newSession(url, key)
if err != nil {
rejectFn.Invoke(err.Error())
return
}
resolve.Invoke(session.jsObject())
}()
return nil
})
return js.Global().Get("Promise").New(handler)
}
type session struct {
client *mosh.Client
conn *wtConn
mu sync.Mutex
closed bool
}
func newSession(url, key string) (*session, error) {
for len(key)%4 != 0 {
key += "="
}
rawKey, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, fmt.Errorf("bad key: %w", err)
}
ocb, err := mosh.NewOCB(rawKey)
if err != nil {
return nil, err
}
conn, err := dialWebTransport(url)
if err != nil {
return nil, err
}
// Use DialConnManual — no internal sendLoop. JS drives Tick() via setInterval.
client, err := mosh.DialConnManual(conn, ocb)
if err != nil {
conn.Close()
return nil, err
}
return &session{client: client, conn: conn}, nil
}
func (s *session) jsObject() js.Value {
obj := js.Global().Get("Object").New()
obj.Set("send", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return nil
}
data := args[0].String()
s.client.Send([]byte(data))
s.client.Tick()
return nil
}))
obj.Set("resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return nil
}
cols := args[0].Int()
rows := args[1].Int()
s.client.Resize(uint16(cols), uint16(rows))
s.client.Tick()
return nil
}))
// tick() — send keepalive. Called by JS setInterval at a slow rate.
obj.Set("tick", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
s.client.Tick()
return nil
}))
// recv(callback) — calls callback(string) whenever output is available
obj.Set("recv", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return nil
}
cb := args[0]
go func() {
for {
out := s.client.Recv(60 * time.Second)
if out == nil {
s.mu.Lock()
closed := s.closed
s.mu.Unlock()
if closed {
return
}
continue
}
cb.Invoke(string(out))
}
}()
return nil
}))
obj.Set("close", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
s.mu.Lock()
s.closed = true
s.mu.Unlock()
s.client.Close()
return nil
}))
return obj
}
func reject(msg string) js.Value {
return js.Global().Get("Promise").Call("reject", msg)
}