- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.1 KiB
Plaintext
159 lines
4.1 KiB
Plaintext
// Five Example: Real-time WebSocket Chat Server
|
|
//
|
|
// Complete chat server in ONE .prg file.
|
|
// Go handles WebSocket, HTTP, concurrency.
|
|
// PRG handles message processing logic.
|
|
//
|
|
// Open http://localhost:9090 in multiple browser tabs to test.
|
|
|
|
PROCEDURE Main()
|
|
? "=== Five WebSocket Chat Server ==="
|
|
? "Open http://localhost:9090 in your browser"
|
|
? "Press Ctrl+C to stop"
|
|
?
|
|
|
|
GoStartChat("9090")
|
|
|
|
RETURN
|
|
|
|
FUNCTION ProcessMessage(cUser, cMessage)
|
|
LOCAL cResult
|
|
|
|
DO CASE
|
|
CASE Upper(Left(cMessage, 5)) == "/HELP"
|
|
cResult := "Commands: /help /time /users /shout <msg>"
|
|
CASE Upper(Left(cMessage, 5)) == "/TIME"
|
|
cResult := "Server time: " + Time() + " " + DToC(Date())
|
|
CASE Upper(Left(cMessage, 6)) == "/SHOUT"
|
|
cResult := Upper(SubStr(cMessage, 7))
|
|
OTHERWISE
|
|
cResult := cMessage
|
|
ENDCASE
|
|
|
|
RETURN "[" + cUser + "] " + cResult
|
|
|
|
#pragma BEGINDUMP
|
|
|
|
import (
|
|
"five/hbrt"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
func init() {
|
|
hbrt.HB_FUNC("GOSTARTCHAT", goStartChat)
|
|
}
|
|
|
|
type chatServer struct {
|
|
mu sync.RWMutex
|
|
clients map[*websocket.Conn]string
|
|
history []string
|
|
}
|
|
|
|
var chat = &chatServer{
|
|
clients: make(map[*websocket.Conn]string),
|
|
}
|
|
|
|
func goStartChat(ctx *hbrt.HBContext) {
|
|
port := ctx.ParC(1)
|
|
if port == "" {
|
|
port = "9090"
|
|
}
|
|
|
|
http.HandleFunc("/", serveHome)
|
|
http.Handle("/ws", websocket.Handler(handleWS))
|
|
|
|
fmt.Printf("Chat server listening on :%s\n", port)
|
|
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
|
ctx.RetC("Error: " + err.Error())
|
|
}
|
|
}
|
|
|
|
func handleWS(ws *websocket.Conn) {
|
|
name := fmt.Sprintf("User_%d", time.Now().UnixNano()%10000)
|
|
chat.mu.Lock()
|
|
chat.clients[ws] = name
|
|
chat.mu.Unlock()
|
|
|
|
broadcast(fmt.Sprintf("* %s joined (%d online) *", name, len(chat.clients)))
|
|
|
|
chat.mu.RLock()
|
|
for _, msg := range chat.history {
|
|
websocket.Message.Send(ws, msg)
|
|
}
|
|
chat.mu.RUnlock()
|
|
|
|
for {
|
|
var msg string
|
|
if err := websocket.Message.Receive(ws, &msg); err != nil {
|
|
break
|
|
}
|
|
if msg == "" {
|
|
continue
|
|
}
|
|
if len(msg) > 6 && msg[:6] == "/name " {
|
|
oldName := name
|
|
name = msg[6:]
|
|
chat.mu.Lock()
|
|
chat.clients[ws] = name
|
|
chat.mu.Unlock()
|
|
broadcast(fmt.Sprintf("* %s is now %s *", oldName, name))
|
|
continue
|
|
}
|
|
broadcast(fmt.Sprintf("[%s] %s", name, msg))
|
|
}
|
|
|
|
chat.mu.Lock()
|
|
delete(chat.clients, ws)
|
|
chat.mu.Unlock()
|
|
broadcast(fmt.Sprintf("* %s left (%d online) *", name, len(chat.clients)))
|
|
}
|
|
|
|
func broadcast(msg string) {
|
|
chat.mu.Lock()
|
|
chat.history = append(chat.history, msg)
|
|
if len(chat.history) > 100 {
|
|
chat.history = chat.history[len(chat.history)-100:]
|
|
}
|
|
snapshot := make(map[*websocket.Conn]bool)
|
|
for k := range chat.clients {
|
|
snapshot[k] = true
|
|
}
|
|
chat.mu.Unlock()
|
|
for ws := range snapshot {
|
|
websocket.Message.Send(ws, msg)
|
|
}
|
|
}
|
|
|
|
func serveHome(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprint(w, chatHTML)
|
|
}
|
|
|
|
const chatHTML = `<!DOCTYPE html>
|
|
<html><head><title>Five Chat</title>
|
|
<style>
|
|
body{font-family:monospace;background:#1a1a2e;color:#eee;margin:20px}
|
|
h1{color:#e94560}
|
|
#log{background:#16213e;padding:15px;height:400px;overflow-y:auto;border:1px solid #0f3460;border-radius:8px;white-space:pre-wrap}
|
|
#msg{width:80%;padding:10px;background:#0f3460;color:#eee;border:1px solid #e94560;border-radius:4px;font-family:monospace}
|
|
button{padding:10px 20px;background:#e94560;color:#fff;border:none;border-radius:4px;cursor:pointer}
|
|
</style></head><body>
|
|
<h1>Five Chat</h1><p>Harbour + Go WebSocket</p>
|
|
<div id="log"></div><br>
|
|
<input id="msg" placeholder="Type message... /name YourName /help" autofocus>
|
|
<button onclick="send()">Send</button>
|
|
<script>
|
|
var ws=new WebSocket("ws://"+location.host+"/ws"),log=document.getElementById("log");
|
|
ws.onmessage=function(e){log.textContent+=e.data+"\n";log.scrollTop=log.scrollHeight};
|
|
ws.onclose=function(){log.textContent+="* Disconnected *\n"};
|
|
function send(){var m=document.getElementById("msg");if(m.value){ws.send(m.value);m.value=""}}
|
|
document.getElementById("msg").onkeypress=function(e){if(e.key==="Enter")send()};
|
|
</script></body></html>`
|
|
|
|
#pragma ENDDUMP
|