feat: enhance WebAssembly API by adding requestPath, setPacketCallback, and setAnnounceCallback functions; refactor SendMessage to SendDataJS for improved data handling
Some checks failed
Bearer / scan (push) Successful in 8s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 43s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 43s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 41s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 44s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Has been cancelled
Go Build Multi-Platform / build (wasm, js) (push) Successful in 51s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 55s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 53s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Has been cancelled
Go Revive Lint / lint (push) Successful in 1m9s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m27s
Run Gosec / tests (push) Successful in 1m31s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Failing after 2m16s
Go Build Multi-Platform / build (amd64, freebsd) (push) Failing after 4m42s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m29s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped

This commit is contained in:
2025-12-31 11:43:27 -06:00
parent 11d4c6407e
commit fd951a10f8
2 changed files with 176 additions and 132 deletions

View File

@@ -21,31 +21,82 @@ var (
reticulumTransport *transport.Transport
reticulumDest *destination.Destination
reticulumIdentity *identity.Identity
userName string
peerMap = make(map[string]string)
stats = struct {
packetsSent int
packetsReceived int
bytesSent int
bytesReceived int
}{}
packetCallback js.Value
announceHandler js.Value
)
// RegisterJSFunctions registers the Reticulum WASM API to the JavaScript global scope.
func RegisterJSFunctions() {
js.Global().Set("reticulum", js.ValueOf(map[string]interface{}{
"init": js.FuncOf(InitReticulum),
"getIdentity": js.FuncOf(GetIdentity),
"getDestination": js.FuncOf(GetDestination),
"announce": js.FuncOf(SendAnnounce),
"connect": js.FuncOf(ConnectWebSocket),
"disconnect": js.FuncOf(DisconnectWebSocket),
"isConnected": js.FuncOf(IsConnected),
"sendMessage": js.FuncOf(SendMessage),
"getStats": js.FuncOf(GetStats),
"init": js.FuncOf(InitReticulum),
"getIdentity": js.FuncOf(GetIdentity),
"getDestination": js.FuncOf(GetDestination),
"connect": js.FuncOf(ConnectWebSocket),
"disconnect": js.FuncOf(DisconnectWebSocket),
"isConnected": js.FuncOf(IsConnected),
"requestPath": js.FuncOf(RequestPath),
"getStats": js.FuncOf(GetStats),
"setPacketCallback": js.FuncOf(SetPacketCallback),
"setAnnounceCallback": js.FuncOf(SetAnnounceCallback),
"sendData": js.FuncOf(SendDataJS),
"announce": js.FuncOf(SendAnnounceJS),
}))
}
func SetPacketCallback(this js.Value, args []js.Value) interface{} {
if len(args) > 0 && args[0].Type() == js.TypeFunction {
packetCallback = args[0]
return js.ValueOf(true)
}
return js.ValueOf(false)
}
func SetAnnounceCallback(this js.Value, args []js.Value) interface{} {
if len(args) > 0 && args[0].Type() == js.TypeFunction {
announceHandler = args[0]
return js.ValueOf(true)
}
return js.ValueOf(false)
}
func RequestPath(this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
return js.ValueOf(map[string]interface{}{
"error": "Destination hash required",
})
}
destHashHex := args[0].String()
destHash, err := hex.DecodeString(destHashHex)
if err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Invalid destination hash: %v", err),
})
}
if reticulumTransport == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
if err := reticulumTransport.RequestPath(destHash, "", nil, true); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to request path: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
})
}
func GetStats(this js.Value, args []js.Value) interface{} {
return js.ValueOf(map[string]interface{}{
"packetsSent": stats.packetsSent,
@@ -68,8 +119,9 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
}
wsURL := args[0].String()
if len(args) >= 2 {
userName = args[1].String()
appName := "wasm_core"
if len(args) >= 2 && args[1].Type() == js.TypeString {
appName = args[1].String()
}
var id *identity.Identity
@@ -105,7 +157,7 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
id,
destination.IN,
destination.SINGLE,
"wasm_core",
appName,
t,
"browser",
)
@@ -115,33 +167,21 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
})
}
if userName != "" {
dest.SetDefaultAppData([]byte(userName))
}
dest.SetPacketCallback(func(data []byte, ni common.NetworkInterface) {
stats.packetsReceived++
stats.bytesReceived += len(data)
var from string
var text string
if len(data) >= 16 {
from = hex.EncodeToString(data[:16])
text = string(data[16:])
} else {
from = ""
text = string(data)
if !packetCallback.IsUndefined() {
// Convert bytes to JS Uint8Array for performance and compatibility
uint8Array := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(uint8Array, data)
packetCallback.Invoke(uint8Array)
}
js.Global().Call("onChatMessage", js.ValueOf(map[string]interface{}{
"text": text,
"from": from,
}))
})
dest.SetProofStrategy(destination.PROVE_ALL)
t.RegisterAnnounceHandler(&announceHandler{})
t.RegisterAnnounceHandler(&genericAnnounceHandler{})
wsInterface, err := interfaces.NewWebSocketInterface("wasm0", wsURL, true)
if err != nil {
@@ -181,6 +221,16 @@ func InitReticulum(this js.Value, args []js.Value) interface{} {
})
}
// GetTransport returns the internal transport pointer.
func GetTransport() *transport.Transport {
return reticulumTransport
}
// GetDestinationPointer returns the internal destination pointer.
func GetDestinationPointer() *destination.Destination {
return reticulumDest
}
func GetIdentity(this js.Value, args []js.Value) interface{} {
if reticulumIdentity == nil {
return js.ValueOf(map[string]interface{}{
@@ -205,34 +255,19 @@ func GetDestination(this js.Value, args []js.Value) interface{} {
})
}
func SendAnnounce(this js.Value, args []js.Value) interface{} {
if reticulumDest == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
func IsConnected(this js.Value, args []js.Value) interface{} {
if reticulumTransport == nil {
return js.ValueOf(false)
}
var appData []byte
if len(args) >= 1 && args[0].String() != "" {
appData = []byte(args[0].String())
userName = args[0].String()
} else if userName != "" {
appData = []byte(userName)
ifaces := reticulumTransport.GetInterfaces()
for _, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(true)
}
}
if len(appData) > 0 {
reticulumDest.SetDefaultAppData(appData)
}
if err := reticulumDest.Announce(false, nil, nil); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to send announce: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
})
return js.ValueOf(false)
}
func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
@@ -246,7 +281,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
for name, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(map[string]interface{}{
"success": true,
"success": true,
"interface": name,
})
}
@@ -256,7 +291,7 @@ func ConnectWebSocket(this js.Value, args []js.Value) interface{} {
})
}
return js.ValueOf(map[string]interface{}{
"success": true,
"success": true,
"interface": name,
})
}
@@ -290,51 +325,36 @@ func DisconnectWebSocket(this js.Value, args []js.Value) interface{} {
})
}
func IsConnected(this js.Value, args []js.Value) interface{} {
if reticulumTransport == nil {
return js.ValueOf(false)
}
type genericAnnounceHandler struct{}
ifaces := reticulumTransport.GetInterfaces()
for _, iface := range ifaces {
if iface.IsOnline() {
return js.ValueOf(true)
}
}
return js.ValueOf(false)
}
type announceHandler struct{}
func (h *announceHandler) AspectFilter() []string {
func (h *genericAnnounceHandler) AspectFilter() []string {
return nil
}
func (h *announceHandler) ReceivePathResponses() bool {
func (h *genericAnnounceHandler) ReceivePathResponses() bool {
return false
}
func (h *announceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte) error {
hashStr := hex.EncodeToString(destHash)
peerMap[hashStr] = string(appData)
js.Global().Call("onPeerDiscovered", js.ValueOf(map[string]interface{}{
"hash": hashStr,
"appData": string(appData),
}))
func (h *genericAnnounceHandler) ReceivedAnnounce(destHash []byte, ident interface{}, appData []byte) error {
if !announceHandler.IsUndefined() {
hashStr := hex.EncodeToString(destHash)
announceHandler.Invoke(js.ValueOf(map[string]interface{}{
"hash": hashStr,
"appData": string(appData),
}))
}
return nil
}
func SendMessage(this js.Value, args []js.Value) interface{} {
// SendDataJS is the JS-facing wrapper for SendData
func SendDataJS(this js.Value, args []js.Value) interface{} {
if len(args) < 2 {
return js.ValueOf(map[string]interface{}{
"error": "Destination hash and message required",
"error": "Destination hash and data required",
})
}
destHashHex := args[0].String()
message := args[1].String()
destHash, err := hex.DecodeString(destHashHex)
if err != nil {
return js.ValueOf(map[string]interface{}{
@@ -342,6 +362,26 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
})
}
// Support both string and Uint8Array data from JS
var data []byte
if args[1].Type() == js.TypeString {
data = []byte(args[1].String())
} else {
data = make([]byte, args[1].Length())
js.CopyBytesToGo(data, args[1])
}
return SendData(destHash, data)
}
// SendData is a generic function to send raw bytes to a destination
func SendData(destHash []byte, data []byte) interface{} {
if reticulumTransport == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
remoteIdentity, err := identity.Recall(destHash)
if err != nil {
return js.ValueOf(map[string]interface{}{
@@ -356,11 +396,7 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
})
}
// Prepend sender hash to message
senderHash := reticulumDest.GetHash()
payload := append(senderHash, []byte(message)...)
encrypted, err := targetDest.Encrypt(payload)
encrypted, err := targetDest.Encrypt(data)
if err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Encryption failed: %v", err),
@@ -393,7 +429,42 @@ func SendMessage(this js.Value, args []js.Value) interface{} {
}
stats.packetsSent++
stats.bytesSent += len(message)
stats.bytesSent += len(data)
return js.ValueOf(map[string]interface{}{
"success": true,
})
}
// SendAnnounceJS is the JS-facing wrapper for SendAnnounce
func SendAnnounceJS(this js.Value, args []js.Value) interface{} {
var appData []byte
if len(args) >= 1 && args[0].Type() == js.TypeString {
appData = []byte(args[0].String())
} else if len(args) >= 1 && args[0].Type() == js.TypeObject {
appData = make([]byte, args[0].Length())
js.CopyBytesToGo(appData, args[0])
}
return SendAnnounce(appData)
}
// SendAnnounce is a generic function to send an announce
func SendAnnounce(appData []byte) interface{} {
if reticulumDest == nil {
return js.ValueOf(map[string]interface{}{
"error": "Reticulum not initialized",
})
}
if len(appData) > 0 {
reticulumDest.SetDefaultAppData(appData)
}
if err := reticulumDest.Announce(false, nil, nil); err != nil {
return js.ValueOf(map[string]interface{}{
"error": fmt.Sprintf("Failed to send announce: %v", err),
})
}
return js.ValueOf(map[string]interface{}{
"success": true,

View File

@@ -22,7 +22,8 @@ func TestRegisterJSFunctions(t *testing.T) {
functions := []string{
"init", "getIdentity", "getDestination", "announce",
"connect", "disconnect", "isConnected", "sendMessage", "getStats",
"connect", "disconnect", "isConnected", "requestPath", "getStats",
"setPacketCallback", "setAnnounceCallback", "sendData",
}
for _, fn := range functions {
@@ -60,9 +61,7 @@ func TestIsConnected(t *testing.T) {
func TestInitReticulum(t *testing.T) {
// Mock JS global functions
js.Global().Set("onChatMessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
js.Global().Set("onPeerDiscovered", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
// Test without arguments
result := InitReticulum(js.Undefined(), []js.Value{})
@@ -71,10 +70,10 @@ func TestInitReticulum(t *testing.T) {
t.Errorf("expected error 'WebSocket URL required', got %v", val.Get("error"))
}
// Test with valid URL and username
// Test with valid URL and app name
wsURL := "ws://localhost:8080"
username := "testuser"
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(username)})
appName := "test_app"
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName)})
val = result.(js.Value)
if !val.Get("success").Bool() {
@@ -84,10 +83,6 @@ func TestInitReticulum(t *testing.T) {
if reticulumIdentity == nil {
t.Fatal("reticulumIdentity should not be nil after successful init")
}
if userName != username {
t.Errorf("expected userName %s, got %s", username, userName)
}
// Test with provided identity
id, _ := identity.NewIdentity()
@@ -96,7 +91,7 @@ func TestInitReticulum(t *testing.T) {
idBytes := id.GetPrivateKey()
idHexFull := hex.EncodeToString(idBytes)
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(username), js.ValueOf(idHexFull)})
result = InitReticulum(js.Undefined(), []js.Value{js.ValueOf(wsURL), js.ValueOf(appName), js.ValueOf(idHexFull)})
val = result.(js.Value)
if !val.Get("success").Bool() {
@@ -110,9 +105,7 @@ func TestInitReticulum(t *testing.T) {
func TestIdentityAndDestination(t *testing.T) {
// Ensure initialized
js.Global().Set("onChatMessage", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
js.Global().Set("log", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
js.Global().Set("onPeerDiscovered", js.FuncOf(func(this js.Value, args []js.Value) interface{} { return nil }))
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
idResult := GetIdentity(js.Undefined(), nil).(js.Value)
@@ -127,21 +120,7 @@ func TestIdentityAndDestination(t *testing.T) {
}
}
func TestAnnounce(t *testing.T) {
// Ensure initialized
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
result := SendAnnounce(js.Undefined(), []js.Value{js.ValueOf("new_username")}).(js.Value)
if !result.Get("success").Bool() {
t.Errorf("SendAnnounce failed: %v", result.Get("error"))
}
if userName != "new_username" {
t.Errorf("userName should have been updated to 'new_username', got %s", userName)
}
}
func TestSendMessage(t *testing.T) {
func TestSendDataJS(t *testing.T) {
// Ensure initialized
InitReticulum(js.Undefined(), []js.Value{js.ValueOf("ws://localhost")})
@@ -153,22 +132,16 @@ func TestSendMessage(t *testing.T) {
// Manually add to known destinations so Recall works
identity.Remember([]byte("mock_packet"), peerHash, peerId.GetPublicKey(), []byte("peer_app_data"))
// Test SendMessage
msg := "Hello Peer!"
result := SendMessage(js.Undefined(), []js.Value{js.ValueOf(peerHashHex), js.ValueOf(msg)}).(js.Value)
// Test SendDataJS with string
data := "Hello Peer!"
result := SendDataJS(js.Undefined(), []js.Value{js.ValueOf(peerHashHex), js.ValueOf(data)}).(js.Value)
if !result.Get("error").IsUndefined() {
errStr := result.Get("error").String()
if errStr != "Packet sending failed: no path to destination" {
t.Errorf("SendMessage failed with unexpected error: %s", errStr)
} else {
t.Log("SendMessage correctly failed with 'no path to destination' (as expected in test environment)")
t.Errorf("SendDataJS failed with unexpected error: %s", errStr)
}
} else if !result.Get("success").Bool() {
t.Errorf("SendMessage failed without error message")
} else {
if stats.packetsSent != 1 {
t.Errorf("expected 1 packet sent, got %d", stats.packetsSent)
}
t.Errorf("SendDataJS failed without error message")
}
}