Server Handlers
Handlers are the core of WSX server functionality. They process incoming WebSocket requests from clients and return responses that update the browser DOM.Basic Handler Setup
Simple Handler
import { html } from "@wsx-sh/core";
wsx.on("button-click", async (request, connection) => {
return {
id: request.id,
target: request.target,
html: html`<div>Button clicked at ${new Date().toLocaleTimeString()}</div>`,
};
});
Default Handler
// Catch-all handler for unmatched requests
wsx.on(async (request, connection) => {
console.log(`Unhandled request: ${request.handler}`);
return {
id: request.id,
target: request.target,
html: html`<div>Handler "${request.handler}" not found</div>`,
};
});
Handler Function Signature
Request Object
interface WSXRequest {
id: string; // Unique request ID
handler: string; // Handler name (from wx-send)
target: string; // Target selector (from wx-target)
trigger: string; // Trigger event type
data?: Record<string, any>; // Form data or custom data
swap?: string; // Swap specification
}
Connection Object
interface WSXConnection {
id: string; // Unique connection ID
sessionData?: Record<string, any>; // Session storage
send(data: string): void; // Send data to client
close(): void; // Close connection
}
Response Object
interface WSXResponse {
id: string; // Must match request.id
target: string; // CSS selector for target
html: string; // HTML content to swap
swap?: string; // How to swap content
oob?: WSXOOBUpdate[]; // Out-of-band updates
}
Handler Patterns
Form Handling
wsx.on("submit-form", async (request, connection) => {
const { username, email, password } = request.data;
// Validate input
if (!username || !email || !password) {
return {
id: request.id,
target: request.target,
html: `<div class="error">All fields are required</div>`,
};
}
try {
// Save to database
const user = await createUser({ username, email, password });
return {
id: request.id,
target: request.target,
html: `<div class="success">User ${user.username} created successfully!</div>`,
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Registration failed: ${error.message}</div>`,
};
}
});
Data Fetching
wsx.on("load-users", async (request, connection) => {
try {
const users = await User.findAll({
order: [["createdAt", "DESC"]],
limit: 20,
});
const usersHtml = users
.map(
(user) => `
<div class="user-card">
<h3>${user.name}</h3>
<p>${user.email}</p>
<button wx-send="delete-user" data-user-id="${user.id}">Delete</button>
</div>
`
)
.join("");
return {
id: request.id,
target: request.target,
html: usersHtml,
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Failed to load users</div>`,
};
}
});
CRUD Operations
// Create
wsx.on("create-post", async (request, connection) => {
const { title, content } = request.data;
const userId = connection.sessionData?.user?.id;
if (!userId) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Authentication required</div>`,
};
}
const post = await Post.create({ title, content, userId });
return {
id: request.id,
target: request.target,
html: `<div class="success">Post "${title}" created</div>`,
oob: [
{
target: "#post-list",
html: `<div class="post">${title}</div>`,
swap: "afterbegin",
},
],
};
});
// Update
wsx.on("update-post", async (request, connection) => {
const { id, title, content } = request.data;
await Post.update({ title, content }, { where: { id } });
return {
id: request.id,
target: request.target,
html: `<div class="success">Post updated</div>`,
};
});
// Delete
wsx.on("delete-post", async (request, connection) => {
const { id } = request.data;
await Post.destroy({ where: { id } });
return {
id: request.id,
target: `#post-${id}`,
html: "",
swap: "outerHTML",
};
});
Advanced Handler Features
Session Management
wsx.on("login", async (request, connection) => {
const { username, password } = request.data;
const user = await authenticate(username, password);
if (user) {
// Store user in session
connection.sessionData = {
...connection.sessionData,
user,
loginTime: new Date(),
};
return {
id: request.id,
target: request.target,
html: `<div>Welcome, ${user.name}!</div>`,
};
} else {
return {
id: request.id,
target: request.target,
html: `<div class="error">Invalid credentials</div>`,
};
}
});
// Use session data in other handlers
wsx.on("get-profile", async (request, connection) => {
const user = connection.sessionData?.user;
if (!user) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Please log in first</div>`,
};
}
return {
id: request.id,
target: request.target,
html: `
<div class="profile">
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
<p>Logged in: ${connection.sessionData.loginTime}</p>
</div>
`,
};
});
Multiple Responses
wsx.on("complex-update", async (request, connection) => {
const updates = await getMultipleUpdates();
// Return array of responses
return updates.map((update) => ({
id: request.id,
target: update.target,
html: update.html,
swap: update.swap,
}));
});
Conditional Logic
wsx.on("admin-action", async (request, connection) => {
const user = connection.sessionData?.user;
// Check permissions
if (!user?.isAdmin) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Admin access required</div>`,
};
}
// Perform admin action
const result = await performAdminAction(request.data);
return {
id: request.id,
target: request.target,
html: `<div class="success">Admin action completed: ${result}</div>`,
};
});
Error Handling
Try-Catch Pattern
wsx.on("database-operation", async (request, connection) => {
try {
const result = await performDatabaseOperation(request.data);
return {
id: request.id,
target: request.target,
html: `<div class="success">Operation completed: ${result.id}</div>`,
};
} catch (error) {
console.error("Database operation failed:", error);
return {
id: request.id,
target: request.target,
html: `<div class="error">Operation failed. Please try again.</div>`,
};
}
});
Validation
wsx.on("validate-data", async (request, connection) => {
const { email, age } = request.data;
// Validation logic
const errors = [];
if (!email || !email.includes("@")) {
errors.push("Valid email is required");
}
if (!age || age < 18) {
errors.push("Age must be 18 or older");
}
if (errors.length > 0) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Validation errors: ${errors.join(", ")}</div>`,
};
}
// Proceed with valid data
const result = await processValidData({ email, age });
return {
id: request.id,
target: request.target,
html: `<div class="success">Data processed successfully</div>`,
};
});
Graceful Degradation
wsx.on("external-api-call", async (request, connection) => {
try {
// Try primary API
const result = await primaryAPI.call(request.data);
return {
id: request.id,
target: request.target,
html: `<div>Primary API result: ${result}</div>`,
};
} catch (primaryError) {
try {
// Fallback to secondary API
const result = await secondaryAPI.call(request.data);
return {
id: request.id,
target: request.target,
html: `<div>Fallback API result: ${result}</div>`,
};
} catch (secondaryError) {
return {
id: request.id,
target: request.target,
html: `<div class="error">All APIs unavailable. Please try again later.</div>`,
};
}
}
});
Handler Middleware
Authentication Middleware
function requireAuth(handler) {
return async (request, connection) => {
const user = connection.sessionData?.user;
if (!user) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Authentication required</div>`,
};
}
return handler(request, connection);
};
}
// Use middleware
wsx.on(
"protected-action",
requireAuth(async (request, connection) => {
return {
id: request.id,
target: request.target,
html: `<div>Protected action executed</div>`,
};
})
);
Validation Middleware
function validateInput(schema) {
return (handler) => async (request, connection) => {
const validation = schema.validate(request.data);
if (validation.error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Validation failed: ${validation.error.message}</div>`,
};
}
return handler(request, connection);
};
}
// Use validation middleware
const userSchema = {
username: { required: true, minLength: 3 },
email: { required: true, type: "email" },
};
wsx.on(
"create-user",
validateInput(userSchema)(async (request, connection) => {
const user = await createUser(request.data);
return {
id: request.id,
target: request.target,
html: `<div>User ${user.username} created</div>`,
};
})
);
Logging Middleware
function logRequests(handler) {
return async (request, connection) => {
const startTime = Date.now();
console.log(
`[${new Date().toISOString()}] ${request.handler} - ${connection.id}`
);
try {
const result = await handler(request, connection);
const duration = Date.now() - startTime;
console.log(
`[${new Date().toISOString()}] ${
request.handler
} completed in ${duration}ms`
);
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(
`[${new Date().toISOString()}] ${
request.handler
} failed after ${duration}ms:`,
error
);
throw error;
}
};
}
Performance Optimization
Caching
const cache = new Map();
wsx.on("expensive-operation", async (request, connection) => {
const cacheKey = `${request.handler}-${JSON.stringify(request.data)}`;
// Check cache first
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
// Perform expensive operation
const result = await performExpensiveOperation(request.data);
const response = {
id: request.id,
target: request.target,
html: `<div>Result: ${result}</div>`,
};
// Cache result for 5 minutes
cache.set(cacheKey, response);
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000);
return response;
});
Database Connection Pooling
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
});
wsx.on("database-query", async (request, connection) => {
const client = await pool.connect();
try {
const result = await client.query("SELECT * FROM users WHERE id = $1", [
request.data.id,
]);
return {
id: request.id,
target: request.target,
html: `<div>User: ${result.rows[0]?.name || "Not found"}</div>`,
};
} finally {
client.release();
}
});
Testing Handlers
Unit Tests
// handler-tests.js
import { jest } from "@jest/globals";
describe("WSX Handlers", () => {
test("should handle user creation", async () => {
const mockRequest = {
id: "test-id",
handler: "create-user",
target: "#result",
data: { username: "testuser", email: "test@example.com" },
};
const mockConnection = {
id: "conn-1",
sessionData: {},
};
const response = await handlers["create-user"](mockRequest, mockConnection);
expect(response.id).toBe("test-id");
expect(response.html).toContain("testuser");
});
test("should require authentication", async () => {
const mockRequest = {
id: "test-id",
handler: "protected-action",
target: "#result",
data: {},
};
const mockConnection = {
id: "conn-1",
sessionData: {}, // No user
};
const response = await handlers["protected-action"](
mockRequest,
mockConnection
);
expect(response.html).toContain("Authentication required");
});
});
Integration Tests
describe("Handler Integration", () => {
test("should update database and return response", async () => {
const wsx = createTestWSXServer();
wsx.on("update-profile", async (request, connection) => {
await User.update(request.data, {
where: { id: connection.sessionData.user.id },
});
return {
id: request.id,
target: request.target,
html: "<div>Profile updated</div>",
};
});
const response = await wsx.handleRequest(
{
id: "test-id",
handler: "update-profile",
target: "#result",
data: { name: "New Name" },
},
mockConnection
);
expect(response.html).toContain("Profile updated");
// Verify database was updated
const user = await User.findByPk(mockConnection.sessionData.user.id);
expect(user.name).toBe("New Name");
});
});
JSON Handlers
In addition to HTML responses, WSX servers can subscribe to JSON message channels. JSON handlers receive a structured payload plus optional metadata and can broadcast messages to other clients.wsx.onJson("presence", async (message, connection) => {
console.log(
`Presence update from ${connection.id}: ${message.data.status}`
);
wsx.broadcastJson("presence", {
userId: connection.id,
status: message.data.status,
});
});
// Catch-all JSON handler
wsx.onJson(async (message, connection) => {
console.log(`JSON on ${message.channel}`, message.data);
});
id: Unique identifier (auto-generated when omitted)channel: Logical name for routingdata: Any JSON-serializable payloadmetadata: Optional contextual information
// Broadcast to all connections
wsx.broadcastJson("presence", { userId: "conn_123", status: "away" });
// Target a single connection and attach metadata
wsx.sendJsonToConnection(connection.id, "presence", { status: "online" }, {
metadata: { since: Date.now() },
});
Stream Handlers
Stream handlers process binary payloads such as audio, video, or sensor readings. Messages arrive with metadata describing the stream plus aUint8Array payload.
wsx.onStream("audio", async (message, data, connection) => {
console.log(
`Audio frame ${message.id} from ${connection.id} (${data.byteLength} bytes)`
);
// Fan the frame back out to listeners
wsx.broadcastStream("audio", data, { metadata: message.metadata });
});
// Observe all streams
wsx.onStream(async (message, data) => {
console.log(`Stream on ${message.channel}`, message.metadata);
});
// Broadcast binary data
wsx.broadcastStream("audio", chunk, {
metadata: { mimeType: "audio/webm" },
});
// Send a frame to a single connection
wsx.sendStreamToConnection(connection.id, "audio", chunk, {
metadata: { sequence: 42 },
});
Uint8Array views—convert to Buffer or copy to an ArrayBuffer when necessary.
Best Practices
- Always Return Responses: Handlers should return a response object
- Handle Errors Gracefully: Use try-catch blocks and provide user-friendly error messages
- Validate Input: Check and validate all incoming data
- Use Session Data: Store connection-specific state in
sessionData - Keep Handlers Focused: Each handler should handle one specific action
- Implement Security: Check permissions and authenticate users
- Log Appropriately: Log errors and important events for debugging
- Test Thoroughly: Write unit and integration tests for handlers
Next Steps
- Learn about Server Broadcasting for multi-client updates
- Explore Server Middleware for request processing
- Understand Out-of-Band Updates for complex responses

