WebSocket

Provides an API for hosting WebSocket connections

Technology

This module provides a comprehensive WebSocket service for Project Forge applications, enabling real-time bidirectional communication between clients and the server.

Features

Architecture

Core Components

Usage

1. Backend Setup

Create Controller Handlers

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Page handler - serves the WebSocket interface
func ExamplePage(w http.ResponseWriter, r *http.Request) {
controller.Act("example", w, r, func(as *app.State, ps *cutil.PageState) (string, error) {
ps.SetTitleAndData("WebSocket Example", nil)
return controller.Render(r, as, &views.ExamplePage{}, ps)
})
}

// WebSocket upgrade handler
func ExampleSocket(w http.ResponseWriter, r *http.Request) {
controller.Act("example.socket", w, r, func(as *app.State, ps *cutil.PageState) (string, error) {
// Get or generate channel
channel := r.URL.Query().Get("ch")
if channel == "" {
channel = "example-" + util.RandomString(8)
}

// Create custom message handler
handler := &ExampleHandler{}

// Upgrade connection
connID, err := as.Services.Socket.Upgrade(
ps.Context, ps.W, ps.R, channel,
ps.Profile, handler, ps.Logger,
)
if err != nil {
return "", err
}

// Start read loop (blocks until connection closes)
return "", as.Services.Socket.ReadLoop(ps.Context, connID, ps.Logger)
})
}

Implement Message Handler

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type ExampleHandler struct{}

func (h *ExampleHandler) On(s *websocket.Service, c *websocket.Connection, cmd string, param []byte, logger util.Logger) error {
switch cmd {
case "ping":
// Echo back a pong
return s.WriteChannel("pong", util.ValueMap{"timestamp": util.TimeCurrentMillis()}, c.Channel, logger)

case "chat":
var msg util.ValueMap
if err := util.FromJSON(param, &msg); err != nil {
return err
}
// Broadcast to all users in channel
return s.WriteChannel("chat", util.ValueMap{
"user": c.Profile.Name,
"text": msg["text"],
"time": util.TimeCurrentMillis(),
}, c.Channel, logger)

case "join-room":
var data util.ValueMap
if err := util.FromJSON(param, &data); err != nil {
return err
}
room := fmt.Sprint(data["room"])
_, err := s.Join(c.ID, room, logger)
return err

default:
logger.Warnf("unhandled websocket command [%s]", cmd)
return nil
}
}

Register Routes

1
2
3
// In your routes setup
makeRoute(r, http.MethodGet, "/example", controller.ExamplePage)
makeRoute(r, http.MethodGet, "/example/socket", controller.ExampleSocket)

2. Frontend Setup

TypeScript Client

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { Socket, Message } from "./socket";

class ExampleClient {
private socket: Socket;
private channel: string;

constructor(channel: string = "example") {
this.channel = channel;

this.socket = new Socket(
true, // debug mode
this.onOpen.bind(this), // connection opened
this.onMessage.bind(this), // message received
this.onError.bind(this), // error occurred
`/example/socket?ch=${channel}` // WebSocket URL
);
}

private onOpen(): void {
console.log("Connected to WebSocket");
this.sendPing();
}

private onMessage(msg: Message): void {
switch (msg.cmd) {
case "pong":
console.log("Received pong:", msg.param);
break;

case "chat":
this.displayChatMessage(msg.param);
break;

case "user-joined":
this.showUserJoined(msg.param);
break;

default:
console.log("Unknown message:", msg);
}
}

private onError(service: string, error: string): void {
console.error(`WebSocket error in ${service}:`, error);
}

// Public methods
sendPing(): void {
this.socket.send({ channel: this.channel, cmd: "ping", param: {} });
}

sendChatMessage(text: string): void {
this.socket.send({
channel: this.channel,
cmd: "chat",
param: { text }
});
}

joinRoom(room: string): void {
this.socket.send({
channel: this.channel,
cmd: "join-room",
param: { room }
});
}
}

// Initialize when page loads
document.addEventListener("DOMContentLoaded", () => {
const client = new ExampleClient("my-channel");

// Wire up UI events
const chatForm = document.getElementById("chat-form") as HTMLFormElement;
const chatInput = document.getElementById("chat-input") as HTMLInputElement;

chatForm?.addEventListener("submit", (e) => {
e.preventDefault();
const text = chatInput.value.trim();
if (text) {
client.sendChatMessage(text);
chatInput.value = "";
}
});
});

HTML Template Integration

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- In your quicktemplate view -->
<div id="websocket-example">
<div id="messages"></div>
<form id="chat-form">
<input type="text" id="chat-input" placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>

<script>
// Simple JavaScript version
const sock = new YourProjectName.Socket(
true,
() => console.log("Connected"),
(msg) => {
const messages = document.getElementById("messages");
const div = document.createElement("div");
div.textContent = `${msg.cmd}: ${JSON.stringify(msg.param)}`;
messages.appendChild(div);
},
(svc, err) => console.error(`${svc}: ${err}`),
"/example/socket"
);

function sendMessage(cmd, param) {
sock.send({ channel: "example", cmd, param });
}
</script>

Service API

Core Methods

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Upgrade HTTP connection to WebSocket
func (s *Service) Upgrade(ctx context.Context, w http.ResponseWriter, r *http.Request,
channel string, profile *user.Profile, handler Handler, logger util.Logger) (uuid.UUID, error)

// Join a channel
func (s *Service) Join(connID uuid.UUID, channel string, logger util.Logger) (bool, error)

// Leave a channel
func (s *Service) Leave(connID uuid.UUID, channel string, logger util.Logger) (bool, error)

// Send message to specific connection
func (s *Service) WriteConnection(cmd string, param any, connID uuid.UUID, logger util.Logger) error

// Send message to all connections in channel
func (s *Service) WriteChannel(cmd string, param any, channel string, logger util.Logger) error

// Broadcast to all connections
func (s *Service) WriteAll(cmd string, param any, logger util.Logger) error

Status and Monitoring

1
2
3
4
5
6
7
8
// Get service status
func (s *Service) Status() *Status

// Get all connections
func (s *Service) GetConnections() []*Connection

// Get connections by channel
func (s *Service) GetConnectionsByChannel(channel string) []*Connection

Admin Interface

The module includes built-in admin pages accessible at:

Best Practices

  1. Error Handling: Always handle WebSocket errors gracefully with reconnection logic
  2. Message Validation: Validate all incoming message parameters
  3. Rate Limiting: Implement rate limiting for message-heavy applications
  4. Channel Management: Use meaningful channel names and clean up unused channels
  5. Security: Validate user permissions before joining channels or processing commands
  6. Performance: Avoid blocking operations in message handlers

Common Patterns

Chat Application

Real-time Updates

Gaming/Collaboration

Notifications

Source Code