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
Copy
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
Copy
// 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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
// 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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
// 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
Copy
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.Copy
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
Copy
// 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.
Copy
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);
});
Copy
// 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