Added NoSocket parameter of the app

This commit is contained in:
anoshenko 2024-03-12 19:32:22 +03:00
parent 30c915d73b
commit ebcba7f9c2
9 changed files with 501 additions and 206 deletions

View File

@ -20,6 +20,9 @@ import (
//go:embed app_socket.js //go:embed app_socket.js
var socketScripts string var socketScripts string
//go:embed app_post.js
var httpPostScripts string
func debugLog(text string) { func debugLog(text string) {
log.Println("\033[34m" + text) log.Println("\033[34m" + text)
} }
@ -28,11 +31,16 @@ func errorLog(text string) {
log.Println("\033[31m" + text) log.Println("\033[31m" + text)
} }
type sessionInfo struct {
session Session
response chan string
}
type application struct { type application struct {
server *http.Server server *http.Server
params AppParams params AppParams
createContentFunc func(Session) SessionContent createContentFunc func(Session) SessionContent
sessions map[int]Session sessions map[int]sessionInfo
} }
func (app *application) getStartPage() string { func (app *application) getStartPage() string {
@ -40,14 +48,18 @@ func (app *application) getStartPage() string {
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
buffer.WriteString("<!DOCTYPE html>\n<html>\n") buffer.WriteString("<!DOCTYPE html>\n<html>\n")
getStartPage(buffer, app.params, socketScripts) getStartPage(buffer, app.params)
buffer.WriteString("\n</html>") buffer.WriteString("\n</html>")
return buffer.String() return buffer.String()
} }
func (app *application) Finish() { func (app *application) Finish() {
for _, session := range app.sessions { for _, session := range app.sessions {
session.close() session.session.close()
if session.response != nil {
close(session.response)
session.response = nil
}
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@ -69,7 +81,12 @@ func (app *application) nextSessionID() int {
} }
func (app *application) removeSession(id int) { func (app *application) removeSession(id int) {
delete(app.sessions, id) if info, ok := app.sessions[id]; ok {
if info.response != nil {
close(info.response)
}
delete(app.sessions, id)
}
} }
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@ -79,6 +96,11 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
switch req.Method { switch req.Method {
case "POST":
if req.URL.Path == "/" {
app.postHandler(w, req)
}
case "GET": case "GET":
switch req.URL.Path { switch req.URL.Path {
case "/": case "/":
@ -86,10 +108,20 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, app.getStartPage()) io.WriteString(w, app.getStartPage())
case "/ws": case "/ws":
if bridge := CreateSocketBridge(w, req); bridge != nil { if bridge := createSocketBridge(w, req); bridge != nil {
go app.socketReader(bridge) go app.socketReader(bridge)
} }
case "/script.js":
w.WriteHeader(http.StatusOK)
if app.params.NoSocket {
io.WriteString(w, httpPostScripts)
} else {
io.WriteString(w, socketScripts)
}
io.WriteString(w, "\n")
io.WriteString(w, defaultScripts)
default: default:
filename := req.URL.Path[1:] filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' { if size := len(filename); size > 0 && filename[size-1] == '/' {
@ -104,7 +136,92 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
} }
func (app *application) socketReader(bridge webBridge) { func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
cookie := http.Cookie{
Name: "session",
Value: strconv.Itoa(sessionID),
HttpOnly: true,
}
http.SetCookie(w, &cookie)
}
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
if reqBody, err := io.ReadAll(req.Body); err == nil {
message := string(reqBody)
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
var session Session = nil
var response chan string = nil
if cookie, err := req.Cookie("session"); err == nil {
sessionID, err := strconv.Atoi(cookie.Value)
if err != nil {
ErrorLog(err.Error())
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
response = info.response
session = info.session
}
}
command := obj.Tag()
if session == nil {
switch command {
case "startSession":
events := make(chan DataObject, 1024)
bridge := createHttpBridge(req)
response = bridge.response
answer := ""
session, answer = app.startSession(obj, events, bridge, response)
bridge.writeMessage(answer)
session.onStart()
bridge.sendResponse()
setSessionIDCookie(w, session.ID())
go sessionEventHandler(session, events, bridge)
default:
return
}
}
switch command {
case "startSession":
case "nop":
session.sendResponse()
/*
case "disconnect":
session.onDisconnect()
return
*/
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
return
default:
if !session.handleAnswer(command, obj) {
session.addToEventsQueue(obj)
}
}
io.WriteString(w, <-response)
for len(response) > 0 {
io.WriteString(w, <-response)
}
}
}
}
func (app *application) socketReader(bridge *wsBridge) {
var session Session var session Session
events := make(chan DataObject, 1024) events := make(chan DataObject, 1024)
@ -116,7 +233,7 @@ func (app *application) socketReader(bridge webBridge) {
} }
if ProtocolInDebugLog { if ProtocolInDebugLog {
DebugLog(message) DebugLog("🖥️ -> " + message)
} }
if obj := ParseDataText(message); obj != nil { if obj := ParseDataText(message); obj != nil {
@ -124,7 +241,7 @@ func (app *application) socketReader(bridge webBridge) {
switch command { switch command {
case "startSession": case "startSession":
answer := "" answer := ""
if session, answer = app.startSession(obj, events, bridge); session != nil { if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) { if !bridge.writeMessage(answer) {
return return
} }
@ -135,7 +252,8 @@ func (app *application) socketReader(bridge webBridge) {
case "reconnect": case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok { if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil { if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil { if info, ok := app.sessions[sessionID]; ok {
session := info.session
session.setBridge(events, bridge) session.setBridge(events, bridge)
answer := allocStringBuilder() answer := allocStringBuilder()
defer freeStringBuilder(answer) defer freeStringBuilder(answer)
@ -157,7 +275,7 @@ func (app *application) socketReader(bridge webBridge) {
} }
answer := "" answer := ""
if session, answer = app.startSession(obj, events, bridge); session != nil { if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) { if !bridge.writeMessage(answer) {
return return
} }
@ -165,23 +283,16 @@ func (app *application) socketReader(bridge webBridge) {
go sessionEventHandler(session, events, bridge) go sessionEventHandler(session, events, bridge)
} }
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj)
case "imageError":
session.imageManager().imageLoadError(obj)
default: default:
events <- obj if !session.handleAnswer(command, obj) {
events <- obj
}
} }
} }
} }
} }
func sessionEventHandler(session Session, events chan DataObject, bridge webBridge) { func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
for { for {
data := <-events data := <-events
@ -201,7 +312,9 @@ func sessionEventHandler(session Session, events chan DataObject, bridge webBrid
} }
} }
func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) { func (app *application) startSession(params DataObject, events chan DataObject,
bridge bridge, response chan string) (Session, string) {
if app.createContentFunc == nil { if app.createContentFunc == nil {
return nil, "" return nil, ""
} }
@ -212,7 +325,10 @@ func (app *application) startSession(params DataObject, events chan DataObject,
return nil, "" return nil, ""
} }
app.sessions[session.ID()] = session app.sessions[session.ID()] = sessionInfo{
session: session,
response: response,
}
answer := allocStringBuilder() answer := allocStringBuilder()
defer freeStringBuilder(answer) defer freeStringBuilder(answer)
@ -236,7 +352,7 @@ var apps = []*application{}
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) { func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application) app := new(application)
app.params = params app.params = params
app.sessions = map[int]Session{} app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc app.createContentFunc = createContentFunc
apps = append(apps, app) apps = append(apps, app)

View File

@ -17,7 +17,7 @@ type wasmApp struct {
params AppParams params AppParams
createContentFunc func(Session) SessionContent createContentFunc func(Session) SessionContent
session Session session Session
bridge webBridge bridge bridge
close chan DataObject close chan DataObject
} }
@ -44,17 +44,10 @@ func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
case "session-close": case "session-close":
app.close <- obj app.close <- obj
case "answer":
app.session.handleAnswer(obj)
case "imageLoaded":
app.session.imageManager().imageLoaded(obj, app.session)
case "imageError":
app.session.imageManager().imageLoadError(obj, app.session)
default: default:
app.session.handleEvent(command, obj) if !app.session.handleAnswer(command, obj) {
app.session.handleEvent(command, obj)
}
} }
} }
} }

75
app_post.js Normal file
View File

@ -0,0 +1,75 @@
function sendMessage(message) {
let xhr = new XMLHttpRequest();
xhr.open('POST', '/', true);
xhr.onreadystatechange = function() {
const script = this.responseText
if (script != "") {
window.eval(script)
//sendMessage("nop{session=" + sessionID +"}")
}
}
xhr.send(message);
}
window.onload = function() {
sendMessage( sessionInfo() );
}
/*
window.onload = function() {
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
const port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = socketOpen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
};
function socketOpen() {
sendMessage( sessionInfo() );
}
function socketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
socket = new WebSocket(socketUrl);
socket.onopen = socketReopen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
}
}
function socketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
window.setTimeout(socketReconnect, 10000);
}
}
*/
function socketError(error) {
console.log(error);
}
window.onfocus = function(event) {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}

View File

@ -1902,10 +1902,28 @@ function getPropertyValue(answerID, elementId, name) {
sendMessage('answer{answerID=' + answerID + ', value=""}') sendMessage('answer{answerID=' + answerID + ', value=""}')
} }
function setStyles(styles) {
document.querySelector('style').textContent = styles
}
function appendStyles(styles) { function appendStyles(styles) {
document.querySelector('style').textContent += styles document.querySelector('style').textContent += styles
} }
function appendAnimationCSS(css) {
let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent += css;
}
}
function setAnimationCSS(css) {
let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent = css;
}
}
function getCanvasContext(elementId) { function getCanvasContext(elementId) {
const canvas = document.getElementById(elementId) const canvas = document.getElementById(elementId)
if (canvas) { if (canvas) {

View File

@ -38,9 +38,12 @@ type AppParams struct {
KeyFile string KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created // Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool Redirect80 bool
// NoSocket - if true then WebSockets will not be used and information exchange
// between the client and the server will be carried out only via http.
NoSocket bool
} }
func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) { func getStartPage(buffer *strings.Builder, params AppParams) {
buffer.WriteString(`<head> buffer.WriteString(`<head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>`) <title>`)
@ -67,11 +70,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
buffer.WriteString(appStyles) buffer.WriteString(appStyles)
buffer.WriteString(`</style> buffer.WriteString(`</style>
<style id="ruiAnimations"></style> <style id="ruiAnimations"></style>
<script> <script src="/script.js"></script>
`)
buffer.WriteString(defaultScripts)
buffer.WriteString(addScripts)
buffer.WriteString(`</script>
</head> </head>
<body id="body" onkeydown="keyDownEvent(this, event)"> <body id="body" onkeydown="keyDownEvent(this, event)">
<div class="ruiRoot" id="ruiRootView"></div> <div class="ruiRoot" id="ruiRootView"></div>

View File

@ -80,6 +80,7 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
manager.images[url] = image manager.images[url] = image
session.callFunc("loadImage", url) session.callFunc("loadImage", url)
session.sendResponse()
return image return image
} }

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
) )
type webBridge interface { type bridge interface {
startUpdateScript(htmlID string) bool startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string) finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool callFunc(funcName string, args ...any) bool
@ -16,10 +16,9 @@ type webBridge interface {
updateCSSProperty(htmlID, property, value string) updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any) updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string) removeProperty(htmlID, property string)
readMessage() (string, bool) sendResponse()
writeMessage(text string) bool setAnimationCSS(css string)
addAnimationCSS(css string) appendAnimationCSS(css string)
clearAnimation()
canvasStart(htmlID string) canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any) callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any) callCanvasVarFunc(v any, funcName string, args ...any)
@ -124,7 +123,7 @@ type Session interface {
nextViewID() string nextViewID() string
styleProperty(styleTag, property string) any styleProperty(styleTag, property string) any
setBridge(events chan DataObject, bridge webBridge) setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder) writeInitScript(writer *strings.Builder)
callFunc(funcName string, args ...any) callFunc(funcName string, args ...any)
updateInnerHTML(htmlID, html string) updateInnerHTML(htmlID, html string)
@ -134,6 +133,7 @@ type Session interface {
removeProperty(htmlID, property string) removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string) finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string) addAnimationCSS(css string)
clearAnimation() clearAnimation()
canvasStart(htmlID string) canvasStart(htmlID string)
@ -145,7 +145,8 @@ type Session interface {
canvasFinish() canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string htmlPropertyValue(htmlID, name string) string
handleAnswer(data DataObject) addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject) handleRootSize(data DataObject)
handleResize(data DataObject) handleResize(data DataObject)
handleEvent(command string, data DataObject) handleEvent(command string, data DataObject)
@ -189,7 +190,7 @@ type sessionData struct {
ignoreUpdates bool ignoreUpdates bool
popups *popupManager popups *popupManager
images *imageManager images *imageManager
bridge webBridge bridge bridge
events chan DataObject events chan DataObject
animationCounter int animationCounter int
animationCSS string animationCSS string
@ -237,7 +238,7 @@ func (session *sessionData) ID() int {
return session.sessionID return session.sessionID
} }
func (session *sessionData) setBridge(events chan DataObject, bridge webBridge) { func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
session.events = events session.events = events
session.bridge = bridge session.bridge = bridge
} }
@ -330,23 +331,19 @@ func (session *sessionData) updateTooltipConstants() {
} }
func (session *sessionData) reload() { func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
css = strings.ReplaceAll(css, "\n", `\n`) session.bridge.callFunc("setStyles", css)
css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
if session.rootView != nil { if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer) viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();") session.bridge.updateInnerHTML("ruiRootView", buffer.String())
session.bridge.callFunc("scanElementsSize")
} }
session.bridge.writeMessage(buffer.String())
session.updateTooltipConstants() session.updateTooltipConstants()
} }
@ -447,15 +444,21 @@ func (session *sessionData) finishUpdateScript(htmlID string) {
} }
} }
func (session *sessionData) sendResponse() {
if session.bridge != nil {
session.bridge.sendResponse()
}
}
func (session *sessionData) addAnimationCSS(css string) { func (session *sessionData) addAnimationCSS(css string) {
if session.bridge != nil { if session.bridge != nil {
session.bridge.addAnimationCSS(css) session.bridge.appendAnimationCSS(css)
} }
} }
func (session *sessionData) clearAnimation() { func (session *sessionData) clearAnimation() {
if session.bridge != nil { if session.bridge != nil {
session.bridge.clearAnimation() session.bridge.setAnimationCSS("")
} }
} }
@ -520,8 +523,23 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
return "" return ""
} }
func (session *sessionData) handleAnswer(data DataObject) { func (session *sessionData) handleAnswer(command string, data DataObject) bool {
session.bridge.answerReceived(data) switch command {
case "answer":
session.bridge.answerReceived(data)
case "imageLoaded":
session.imageManager().imageLoaded(data)
case "imageError":
session.imageManager().imageLoadError(data)
default:
return false
}
session.bridge.sendResponse()
return true
} }
func (session *sessionData) handleRootSize(data DataObject) { func (session *sessionData) handleRootSize(data DataObject) {
@ -672,6 +690,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
ErrorLog(`"id" property not found. Event: ` + command) ErrorLog(`"id" property not found. Event: ` + command)
} }
} }
session.bridge.sendResponse()
} }
func (session *sessionData) hotKey(event KeyEvent) { func (session *sessionData) hotKey(event KeyEvent) {
@ -769,3 +789,7 @@ func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{} session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear") session.bridge.callFunc("localStorageClear")
} }
func (session *sessionData) addToEventsQueue(data DataObject) {
session.events <- data
}

View File

@ -16,7 +16,7 @@ type wasmBridge struct {
closeEvent chan DataObject closeEvent chan DataObject
} }
func createWasmBridge(close chan DataObject) webBridge { func createWasmBridge(close chan DataObject) bridge {
bridge := new(wasmBridge) bridge := new(wasmBridge)
bridge.answerID = 1 bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject) bridge.answer = make(map[int]chan DataObject)
@ -102,10 +102,6 @@ func (bridge *wasmBridge) close() {
bridge.closeEvent <- NewDataObject("close") bridge.closeEvent <- NewDataObject("close")
} }
func (bridge *wasmBridge) readMessage() (string, bool) {
return "", false
}
func (bridge *wasmBridge) writeMessage(script string) bool { func (bridge *wasmBridge) writeMessage(script string) bool {
if ProtocolInDebugLog { if ProtocolInDebugLog {
DebugLog("Run script:") DebugLog("Run script:")
@ -118,21 +114,24 @@ func (bridge *wasmBridge) writeMessage(script string) bool {
return true return true
} }
func (bridge *wasmBridge) addAnimationCSS(css string) { func (bridge *wasmBridge) prepareCSS(css string) string {
css = strings.ReplaceAll(css, `\t`, "\t") css = strings.ReplaceAll(css, `\t`, "\t")
css = strings.ReplaceAll(css, `\n`, "\n") css = strings.ReplaceAll(css, `\n`, "\n")
css = strings.ReplaceAll(css, `\'`, "'") css = strings.ReplaceAll(css, `\'`, "'")
css = strings.ReplaceAll(css, `\"`, "\"") css = strings.ReplaceAll(css, `\"`, "\"")
css = strings.ReplaceAll(css, `\\`, "\\") css = strings.ReplaceAll(css, `\\`, "\\")
return css
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
content := styles.Get("textContent").String()
styles.Set("textContent", content+"\n"+css)
} }
func (bridge *wasmBridge) clearAnimation() { func (bridge *wasmBridge) appendAnimationCSS(css string) {
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations") styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
styles.Set("textContent", "") content := styles.Get("textContent").String()
styles.Set("textContent", content+"\n"+bridge.prepareCSS(css))
}
func (bridge *wasmBridge) setAnimationCSS(css string) {
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
styles.Set("textContent", bridge.prepareCSS(css))
} }
func (bridge *wasmBridge) canvasStart(htmlID string) { func (bridge *wasmBridge) canvasStart(htmlID string) {
@ -276,3 +275,6 @@ func (bridge *wasmBridge) answerReceived(answer DataObject) {
func (bridge *wasmBridge) remoteAddr() string { func (bridge *wasmBridge) remoteAddr() string {
return "localhost" return "localhost"
} }
func (bridge *wasmBridge) sendResponse() {
}

View File

@ -12,17 +12,30 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
type webBridge struct {
answer map[int]chan DataObject
answerID int
answerMutex sync.Mutex
writeMutex sync.Mutex
closed bool
canvasBuffer strings.Builder
canvasVarNumber int
updateScripts map[string]*strings.Builder
writeMessage func(string) bool
callFuncImmediately func(funcName string, args ...any) bool
}
type wsBridge struct { type wsBridge struct {
conn *websocket.Conn webBridge
answer map[int]chan DataObject conn *websocket.Conn
answerID int }
answerMutex sync.Mutex
writeMutex sync.Mutex type httpBridge struct {
closed bool webBridge
buffer strings.Builder responseBuffer strings.Builder
canvasBuffer strings.Builder response chan string
canvasVarNumber int remoteAddress string
updateScripts map[string]*strings.Builder //conn *websocket.Conn
} }
type canvasVar struct { type canvasVar struct {
@ -34,7 +47,7 @@ var upgrader = websocket.Upgrader{
WriteBufferSize: 8096, WriteBufferSize: 8096,
} }
func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge { func createSocketBridge(w http.ResponseWriter, req *http.Request) *wsBridge {
conn, err := upgrader.Upgrade(w, req, nil) conn, err := upgrader.Upgrade(w, req, nil)
if err != nil { if err != nil {
ErrorLog(err.Error()) ErrorLog(err.Error())
@ -42,42 +55,84 @@ func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
} }
bridge := new(wsBridge) bridge := new(wsBridge)
bridge.answerID = 1 bridge.initBridge()
bridge.answer = make(map[int]chan DataObject)
bridge.conn = conn bridge.conn = conn
bridge.closed = false bridge.writeMessage = func(script string) bool {
bridge.updateScripts = map[string]*strings.Builder{} if ProtocolInDebugLog {
DebugLog("🖥️ <- " + script)
}
if bridge.conn == nil {
ErrorLog("No connection")
return false
}
bridge.writeMutex.Lock()
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
bridge.writeMutex.Unlock()
if err != nil {
ErrorLog(err.Error())
return false
}
return true
}
bridge.callFuncImmediately = bridge.callFunc
return bridge return bridge
} }
func (bridge *wsBridge) close() { func createHttpBridge(req *http.Request) *httpBridge {
bridge.closed = true bridge := new(httpBridge)
defer bridge.conn.Close() bridge.initBridge()
bridge.conn = nil bridge.response = make(chan string, 10)
bridge.writeMessage = func(script string) bool {
if script != "" {
if ProtocolInDebugLog {
DebugLog(script)
}
if bridge.responseBuffer.Len() > 0 {
bridge.responseBuffer.WriteRune('\n')
}
bridge.responseBuffer.WriteString(script)
}
return true
}
bridge.callFuncImmediately = bridge.callImmediately
bridge.remoteAddress = req.RemoteAddr
return bridge
} }
func (bridge *wsBridge) startUpdateScript(htmlID string) bool { func (bridge *webBridge) initBridge() {
bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject)
bridge.closed = false
bridge.updateScripts = map[string]*strings.Builder{}
}
func (bridge *webBridge) startUpdateScript(htmlID string) bool {
if _, ok := bridge.updateScripts[htmlID]; ok { if _, ok := bridge.updateScripts[htmlID]; ok {
return false return false
} }
buffer := allocStringBuilder() buffer := allocStringBuilder()
bridge.updateScripts[htmlID] = buffer bridge.updateScripts[htmlID] = buffer
buffer.WriteString("let element = document.getElementById('") buffer.WriteString("{\nlet element = document.getElementById('")
buffer.WriteString(htmlID) buffer.WriteString(htmlID)
buffer.WriteString("');\nif (element) {\n") buffer.WriteString("');\nif (element) {\n")
return true return true
} }
func (bridge *wsBridge) finishUpdateScript(htmlID string) { func (bridge *webBridge) finishUpdateScript(htmlID string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok { if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString("scanElementsSize();\n}\n") buffer.WriteString("scanElementsSize();\n}\n}\n")
bridge.writeMessage(buffer.String()) bridge.writeMessage(buffer.String())
freeStringBuilder(buffer) freeStringBuilder(buffer)
delete(bridge.updateScripts, htmlID) delete(bridge.updateScripts, htmlID)
} }
} }
func (bridge *wsBridge) argToString(arg any) (string, bool) { func (bridge *webBridge) argToString(arg any) (string, bool) {
switch arg := arg.(type) { switch arg := arg.(type) {
case string: case string:
arg = strings.ReplaceAll(arg, "\\", `\\`) arg = strings.ReplaceAll(arg, "\\", `\\`)
@ -152,47 +207,44 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
return "", false return "", false
} }
func (bridge *wsBridge) callFunc(funcName string, args ...any) bool { func (bridge *webBridge) callFuncScript(funcName string, args ...any) (string, bool) {
bridge.buffer.Reset() buffer := allocStringBuilder()
bridge.buffer.WriteString(funcName) defer freeStringBuilder(buffer)
bridge.buffer.WriteRune('(')
buffer.WriteString(funcName)
buffer.WriteRune('(')
for i, arg := range args { for i, arg := range args {
argText, ok := bridge.argToString(arg) argText, ok := bridge.argToString(arg)
if !ok { if !ok {
return false return "", false
} }
if i > 0 { if i > 0 {
bridge.buffer.WriteString(", ") buffer.WriteString(", ")
} }
bridge.buffer.WriteString(argText) buffer.WriteString(argText)
} }
bridge.buffer.WriteString(");") buffer.WriteString(");")
funcText := bridge.buffer.String() return buffer.String(), true
if ProtocolInDebugLog {
DebugLog("Run func: " + funcText)
}
bridge.writeMutex.Lock()
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(funcText))
bridge.writeMutex.Unlock()
if err != nil {
ErrorLog(err.Error())
return false
}
return true
} }
func (bridge *wsBridge) updateInnerHTML(htmlID, html string) { func (bridge *webBridge) callFunc(funcName string, args ...any) bool {
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
return bridge.writeMessage(funcText)
}
return false
}
func (bridge *webBridge) updateInnerHTML(htmlID, html string) {
bridge.callFunc("updateInnerHTML", htmlID, html) bridge.callFunc("updateInnerHTML", htmlID, html)
} }
func (bridge *wsBridge) appendToInnerHTML(htmlID, html string) { func (bridge *webBridge) appendToInnerHTML(htmlID, html string) {
bridge.callFunc("appendToInnerHTML", htmlID, html) bridge.callFunc("appendToInnerHTML", htmlID, html)
} }
func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) { func (bridge *webBridge) updateCSSProperty(htmlID, property, value string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok { if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`element.style['`) buffer.WriteString(`element.style['`)
buffer.WriteString(property) buffer.WriteString(property)
@ -204,7 +256,7 @@ func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
} }
} }
func (bridge *wsBridge) updateProperty(htmlID, property string, value any) { func (bridge *webBridge) updateProperty(htmlID, property string, value any) {
if buffer, ok := bridge.updateScripts[htmlID]; ok { if buffer, ok := bridge.updateScripts[htmlID]; ok {
if val, ok := bridge.argToString(value); ok { if val, ok := bridge.argToString(value); ok {
buffer.WriteString(`element.setAttribute('`) buffer.WriteString(`element.setAttribute('`)
@ -218,7 +270,7 @@ func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
} }
} }
func (bridge *wsBridge) removeProperty(htmlID, property string) { func (bridge *webBridge) removeProperty(htmlID, property string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok { if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`if (element.hasAttribute('`) buffer.WriteString(`if (element.hasAttribute('`)
buffer.WriteString(property) buffer.WriteString(property)
@ -230,28 +282,34 @@ func (bridge *wsBridge) removeProperty(htmlID, property string) {
} }
} }
func (bridge *wsBridge) addAnimationCSS(css string) { func (bridge *webBridge) appendAnimationCSS(css string) {
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations'); //bridge.callFunc("appendAnimationCSS", css)
if (styles) { bridge.writeMessage(`{
styles.textContent += '` + css + `'; let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent += '` + css + `';
}
}`) }`)
} }
func (bridge *wsBridge) clearAnimation() { func (bridge *webBridge) setAnimationCSS(css string) {
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations'); //bridge.callFunc("setAnimationCSS", css)
if (styles) { bridge.writeMessage(`{
styles.textContent = ''; let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent = '` + css + `';
}
}`) }`)
} }
func (bridge *wsBridge) canvasStart(htmlID string) { func (bridge *webBridge) canvasStart(htmlID string) {
bridge.canvasBuffer.Reset() bridge.canvasBuffer.Reset()
bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`) bridge.canvasBuffer.WriteString("{\nconst ctx = getCanvasContext('")
bridge.canvasBuffer.WriteString(htmlID) bridge.canvasBuffer.WriteString(htmlID)
bridge.canvasBuffer.WriteString(`');`) bridge.canvasBuffer.WriteString(`');`)
} }
func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) { func (bridge *webBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nctx.") bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(funcName) bridge.canvasBuffer.WriteString(funcName)
bridge.canvasBuffer.WriteRune('(') bridge.canvasBuffer.WriteRune('(')
@ -265,7 +323,7 @@ func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");") bridge.canvasBuffer.WriteString(");")
} }
func (bridge *wsBridge) updateCanvasProperty(property string, value any) { func (bridge *webBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString("\nctx.") bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(property) bridge.canvasBuffer.WriteString(property)
bridge.canvasBuffer.WriteString(" = ") bridge.canvasBuffer.WriteString(" = ")
@ -274,7 +332,7 @@ func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString(";") bridge.canvasBuffer.WriteString(";")
} }
func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any { func (bridge *webBridge) createCanvasVar(funcName string, args ...any) any {
bridge.canvasVarNumber++ bridge.canvasVarNumber++
result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)} result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)}
bridge.canvasBuffer.WriteString("\nlet ") bridge.canvasBuffer.WriteString("\nlet ")
@ -293,7 +351,7 @@ func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
return result return result
} }
func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) { func (bridge *webBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
varName, ok := v.(canvasVar) varName, ok := v.(canvasVar)
if !ok { if !ok {
return return
@ -313,7 +371,7 @@ func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");") bridge.canvasBuffer.WriteString(");")
} }
func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) { func (bridge *webBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nimg = images.get('") bridge.canvasBuffer.WriteString("\nimg = images.get('")
bridge.canvasBuffer.WriteString(url) bridge.canvasBuffer.WriteString(url)
@ -334,56 +392,12 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
bridge.canvasBuffer.WriteString(");\n}") bridge.canvasBuffer.WriteString(");\n}")
} }
func (bridge *wsBridge) canvasFinish() { func (bridge *webBridge) canvasFinish() {
bridge.canvasBuffer.WriteString("\n") bridge.canvasBuffer.WriteString("\n}\n")
script := bridge.canvasBuffer.String() bridge.writeMessage(bridge.canvasBuffer.String())
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
}
bridge.writeMutex.Lock()
if bridge.conn == nil {
ErrorLog("No connection")
} else if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
ErrorLog(err.Error())
}
bridge.writeMutex.Unlock()
} }
func (bridge *wsBridge) readMessage() (string, bool) { func (bridge *webBridge) removeValue(funcName, htmlID string, args ...string) (DataObject, bool) {
_, p, err := bridge.conn.ReadMessage()
if err != nil {
if !bridge.closed {
ErrorLog(err.Error())
}
return "", false
}
return string(p), true
}
func (bridge *wsBridge) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
}
if bridge.conn == nil {
ErrorLog("No connection")
return false
}
bridge.writeMutex.Lock()
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
bridge.writeMutex.Unlock()
if err != nil {
ErrorLog(err.Error())
return false
}
return true
}
func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
result := TextMetrics{}
bridge.answerMutex.Lock() bridge.answerMutex.Lock()
answerID := bridge.answerID answerID := bridge.answerID
bridge.answerID++ bridge.answerID++
@ -392,36 +406,40 @@ func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics
answer := make(chan DataObject) answer := make(chan DataObject)
bridge.answer[answerID] = answer bridge.answer[answerID] = answer
if bridge.callFunc("canvasTextMetrics", answerID, htmlID, font, text) { funcArgs := []any{answerID, htmlID}
data := <-answer for _, arg := range args {
result.Width = dataFloatProperty(data, "width") funcArgs = append(funcArgs, arg)
} }
var result DataObject = nil
ok := bridge.callFuncImmediately(funcName, funcArgs...)
if ok {
result = <-answer
}
close(answer)
delete(bridge.answer, answerID) delete(bridge.answer, answerID)
return result, true
}
func (bridge *webBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
result := TextMetrics{}
if data, ok := bridge.removeValue("canvasTextMetrics", htmlID, font, text); ok {
result.Width = dataFloatProperty(data, "width")
}
return result return result
} }
func (bridge *wsBridge) htmlPropertyValue(htmlID, name string) string { func (bridge *webBridge) htmlPropertyValue(htmlID, name string) string {
bridge.answerMutex.Lock() if data, ok := bridge.removeValue("getPropertyValue", htmlID, name); ok {
answerID := bridge.answerID
bridge.answerID++
bridge.answerMutex.Unlock()
answer := make(chan DataObject)
bridge.answer[answerID] = answer
if bridge.callFunc("getPropertyValue", answerID, htmlID, name) {
data := <-answer
if value, ok := data.PropertyValue("value"); ok { if value, ok := data.PropertyValue("value"); ok {
return value return value
} }
} }
delete(bridge.answer, answerID)
return "" return ""
} }
func (bridge *wsBridge) answerReceived(answer DataObject) { func (bridge *webBridge) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok { if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil { if id, err := strconv.Atoi(text); err == nil {
if chanel, ok := bridge.answer[id]; ok { if chanel, ok := bridge.answer[id]; ok {
@ -438,6 +456,55 @@ func (bridge *wsBridge) answerReceived(answer DataObject) {
} }
} }
func (bridge *wsBridge) close() {
bridge.closed = true
defer bridge.conn.Close()
bridge.conn = nil
}
func (bridge *wsBridge) readMessage() (string, bool) {
_, p, err := bridge.conn.ReadMessage()
if err != nil {
if !bridge.closed {
ErrorLog(err.Error())
}
return "", false
}
return string(p), true
}
func (bridge *wsBridge) sendResponse() {
}
func (bridge *wsBridge) remoteAddr() string { func (bridge *wsBridge) remoteAddr() string {
return bridge.conn.RemoteAddr().String() return bridge.conn.RemoteAddr().String()
} }
func (bridge *httpBridge) close() {
bridge.closed = true
// TODO
}
func (bridge *httpBridge) callImmediately(funcName string, args ...any) bool {
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
if ProtocolInDebugLog {
DebugLog("Run func: " + funcText)
}
bridge.response <- funcText
return true
}
return false
}
func (bridge *httpBridge) sendResponse() {
bridge.writeMutex.Lock()
text := bridge.responseBuffer.String()
bridge.responseBuffer.Reset()
bridge.writeMutex.Unlock()
bridge.response <- text
}
func (bridge *httpBridge) remoteAddr() string {
return bridge.remoteAddress
}