Using WSX with Hono framework for edge computing
npm install @wsx-sh/core @wsx-sh/hono
import { createHonoWSXServer } from "@wsx-sh/hono";
import { Hono } from "hono";
// Create WSX server with Hono adapter
const wsx = createHonoWSXServer({
path: "/ws",
});
const app = wsx.getApp();
// Add WSX handlers
wsx.on("hello", async (request, connection) => {
return {
id: request.id,
target: request.target,
html: `<div>Hello from Hono at ${new Date().toISOString()}!</div>`,
};
});
// Regular Hono routes
app.get("/", (c) => {
return c.html(`
<!DOCTYPE html>
<html>
<head>
<title>WSX with Hono</title>
<script src="/wsx.js"></script>
</head>
<body>
<div wx-config='{"url": "ws://localhost:8787/ws"}'>
<div id="content">Click the button below</div>
<button wx-send="hello" wx-target="#content">Say Hello</button>
</div>
</body>
</html>
`);
});
// Serve static files
app.get("/wsx.js", (c) => {
return c.text(getWSXClientScript());
});
export default app;
// wrangler.toml
[env.production]
name = "wsx-hono-app"
main = "src/index.js"
compatibility_date = "2024-01-01"
[env.production.durable_objects]
bindings = [
{name = "WSX_CONNECTIONS", class_name = "WSXConnections"}
]
// src/index.js
import { createHonoWSXServer } from "@wsx-sh/hono";
const wsx = createHonoWSXServer({
path: '/ws',
durableObjects: {
namespace: 'WSX_CONNECTIONS'
}
});
const app = wsx.getApp();
// Chat application
wsx.on("send-message", async (request, connection) => {
const { message, room } = request.data;
const user = connection.sessionData?.user || { name: 'Anonymous' };
const messageHtml = `
<div class="message">
<strong>${user.name}:</strong> ${message}
<span class="timestamp">${new Date().toLocaleTimeString()}</span>
</div>
`;
// Broadcast to all connections in room
const roomConnections = wsx.getConnections()
.filter(conn => conn.sessionData?.room === room);
roomConnections.forEach(conn => {
wsx.sendToConnection(conn.id, "#messages", messageHtml, "beforeend");
});
return {
id: request.id,
target: "#message-input",
html: `<input type="text" placeholder="Type a message..." />`,
swap: "outerHTML"
};
});
// Join room
wsx.on("join-room", async (request, connection) => {
const { room, username } = request.data;
connection.sessionData = {
...connection.sessionData,
room,
user: { name: username }
};
return {
id: request.id,
target: request.target,
html: `<div>Joined room: ${room}</div>`
};
});
export default app;
// deno.json
{
"tasks": {
"dev": "deno run --allow-net --allow-read --watch src/main.ts",
"start": "deno run --allow-net --allow-read src/main.ts"
},
"imports": {
"hono": "https://deno.land/x/hono@v4.0.0/mod.ts",
"@wsx-sh/hono": "https://deno.land/x/wsx@v1.0.0/hono.ts"
}
}
// src/main.ts
import { createHonoWSXServer } from "@wsx-sh/hono";
import { serve } from "https://deno.land/std@0.200.0/http/server.ts";
const wsx = createHonoWSXServer({
path: '/ws'
});
const app = wsx.getApp();
// Real-time dashboard
wsx.on("get-stats", async (request, connection) => {
const stats = await getSystemStats();
const statsHtml = `
<div class="stats-grid">
<div class="stat">
<h3>Memory Usage</h3>
<div class="progress">
<div class="bar" style="width: ${stats.memory.percent}%"></div>
</div>
<span>${stats.memory.used}MB / ${stats.memory.total}MB</span>
</div>
<div class="stat">
<h3>CPU Usage</h3>
<div class="progress">
<div class="bar" style="width: ${stats.cpu.percent}%"></div>
</div>
<span>${stats.cpu.percent}%</span>
</div>
</div>
`;
return {
id: request.id,
target: request.target,
html: statsHtml
};
});
// Auto-refresh stats every 5 seconds
setInterval(async () => {
const connections = wsx.getConnections();
const stats = await getSystemStats();
connections.forEach(conn => {
wsx.sendToConnection(conn.id, "#live-stats", generateStatsHTML(stats));
});
}, 5000);
async function getSystemStats() {
const memInfo = Deno.memoryUsage();
return {
memory: {
used: Math.round(memInfo.heapUsed / 1024 / 1024),
total: Math.round(memInfo.heapTotal / 1024 / 1024),
percent: Math.round((memInfo.heapUsed / memInfo.heapTotal) * 100)
},
cpu: {
percent: Math.round(Math.random() * 100) // Mock CPU usage
}
};
}
serve(app.fetch, { port: 8000 });
// package.json
{
"name": "wsx-hono-bun",
"type": "module",
"scripts": {
"dev": "bun run --watch src/index.js",
"start": "bun run src/index.js"
},
"dependencies": {
"hono": "^4.0.0",
"@wsx-sh/hono": "^1.0.0"
}
}
// src/index.js
import { createHonoWSXServer } from "@wsx-sh/hono";
const wsx = createHonoWSXServer({
path: '/ws'
});
const app = wsx.getApp();
// File upload with real-time progress
wsx.on("upload-file", async (request, connection) => {
const { file, filename } = request.data;
try {
// Write file using Bun's optimized file operations
const path = `./uploads/${filename}`;
await Bun.write(path, file);
// Broadcast upload completion
wsx.broadcast("#upload-status", `
<div class="upload-success">
File "${filename}" uploaded successfully!
</div>
`);
return {
id: request.id,
target: request.target,
html: `<div>File uploaded: ${filename}</div>`
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Upload failed: ${error.message}</div>`
};
}
});
// Real-time file processing
wsx.on("process-file", async (request, connection) => {
const { filename } = request.data;
// Send processing updates
wsx.sendToConnection(connection.id, "#status",
`<div>Processing ${filename}...</div>`);
// Process file (example: image optimization)
const result = await processFile(filename);
return {
id: request.id,
target: request.target,
html: `<div>File processed: ${result.outputPath}</div>`
};
});
export default {
port: 3000,
fetch: app.fetch
};
import { jwt } from "hono/jwt";
const app = wsx.getApp();
// JWT authentication middleware
app.use(
"/api/*",
jwt({
secret: process.env.JWT_SECRET,
})
);
// Protected API routes
app.get("/api/profile", async (c) => {
const user = c.get("jwtPayload");
return c.json({ user });
});
// WSX authentication
wsx.onConnection = async (connection) => {
const token = extractTokenFromConnection(connection);
if (token) {
try {
const payload = await verifyJWT(token);
connection.sessionData = { user: payload };
} catch (error) {
connection.close(1008, "Invalid token");
}
}
};
function extractTokenFromConnection(connection) {
// Extract from query parameter or header
const url = new URL(connection.request.url);
return url.searchParams.get("token");
}
import { cors } from "hono/cors";
app.use(
"/api/*",
cors({
origin: ["https://your-domain.com", "https://www.your-domain.com"],
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
})
);
import { rateLimiter } from "hono/rate-limiter";
// Rate limiting for API routes
app.use(
"/api/*",
rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many requests from this IP",
})
);
// WSX-specific rate limiting
const wsxRateLimiter = new Map();
wsx.on("rate-limited-action", async (request, connection) => {
const ip = connection.ip || "unknown";
const key = `${ip}:${request.handler}`;
const now = Date.now();
const limit = wsxRateLimiter.get(key);
if (limit && now - limit.lastReset < 60000) {
// 1 minute window
if (limit.count >= 10) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Rate limit exceeded</div>`,
};
}
limit.count++;
} else {
wsxRateLimiter.set(key, { count: 1, lastReset: now });
}
return {
id: request.id,
target: request.target,
html: `<div>Action completed</div>`,
};
});
// wrangler.toml
[env.production.d1_databases];
binding = "DB";
database_name = "wsx-app";
database_id = "your-database-id";
// Handler with D1 database
wsx.on("create-post", async (request, connection, env) => {
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>`,
};
}
try {
const result = await env.DB.prepare(
"INSERT INTO posts (title, content, user_id, created_at) VALUES (?, ?, ?, ?)"
)
.bind(title, content, userId, new Date().toISOString())
.run();
return {
id: request.id,
target: request.target,
html: `<div>Post created with ID: ${result.meta.last_row_id}</div>`,
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Failed to create post</div>`,
};
}
});
// Using Deno KV for persistence
const kv = await Deno.openKv();
wsx.on("save-data", async (request, connection) => {
const { key, value } = request.data;
const userId = connection.sessionData?.user?.id;
if (!userId) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Authentication required</div>`,
};
}
try {
await kv.set([`user:${userId}`, key], value);
return {
id: request.id,
target: request.target,
html: `<div>Data saved successfully</div>`,
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Failed to save data</div>`,
};
}
});
wsx.on("load-data", async (request, connection) => {
const { key } = request.data;
const userId = connection.sessionData?.user?.id;
if (!userId) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Authentication required</div>`,
};
}
try {
const result = await kv.get([`user:${userId}`, key]);
return {
id: request.id,
target: request.target,
html: `<div>Data: ${JSON.stringify(result.value)}</div>`,
};
} catch (error) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Failed to load data</div>`,
};
}
});
// Collaborative document editing
const documents = new Map();
wsx.on("join-document", async (request, connection) => {
const { documentId } = request.data;
const user = connection.sessionData?.user;
if (!user) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Authentication required</div>`,
};
}
// Track document participants
if (!documents.has(documentId)) {
documents.set(documentId, new Set());
}
documents.get(documentId).add(connection.id);
connection.sessionData.documentId = documentId;
// Notify other participants
const participants = Array.from(documents.get(documentId))
.map((id) => wsx.getConnection(id))
.filter((conn) => conn && conn.id !== connection.id);
participants.forEach((conn) => {
wsx.sendToConnection(
conn.id,
"#participants",
`<div>${user.name} joined the document</div>`,
"beforeend"
);
});
return {
id: request.id,
target: request.target,
html: `<div>Joined document: ${documentId}</div>`,
};
});
wsx.on("document-change", async (request, connection) => {
const { change, position } = request.data;
const documentId = connection.sessionData?.documentId;
const user = connection.sessionData?.user;
if (!documentId || !user) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Invalid session</div>`,
};
}
// Broadcast change to other participants
const participants = Array.from(documents.get(documentId) || [])
.map((id) => wsx.getConnection(id))
.filter((conn) => conn && conn.id !== connection.id);
const changeHtml = `
<div class="document-change" data-position="${position}">
<span class="user">${user.name}</span> made a change
</div>
`;
participants.forEach((conn) => {
wsx.sendToConnection(conn.id, "#document-changes", changeHtml, "beforeend");
});
return {
id: request.id,
target: request.target,
html: `<div>Change synchronized</div>`,
};
});
// Real-time analytics dashboard
let dashboardInterval;
wsx.on("start-dashboard", async (request, connection) => {
const user = connection.sessionData?.user;
if (!user?.isAdmin) {
return {
id: request.id,
target: request.target,
html: `<div class="error">Admin access required</div>`,
};
}
// Start sending real-time updates
dashboardInterval = setInterval(async () => {
const stats = await getAnalytics();
const dashboardHtml = `
<div class="dashboard-stats">
<div class="metric">
<h3>Active Users</h3>
<span class="value">${stats.activeUsers}</span>
</div>
<div class="metric">
<h3>Requests/sec</h3>
<span class="value">${stats.requestsPerSecond}</span>
</div>
<div class="metric">
<h3>Error Rate</h3>
<span class="value">${stats.errorRate}%</span>
</div>
</div>
`;
wsx.sendToConnection(connection.id, "#dashboard", dashboardHtml);
}, 1000);
return {
id: request.id,
target: request.target,
html: `<div>Dashboard started</div>`,
};
});
wsx.onDisconnection = (connection) => {
if (dashboardInterval) {
clearInterval(dashboardInterval);
}
};
app.onError = (err, c) => {
console.error("Hono error:", err);
return c.json(
{
error: "Internal server error",
message:
process.env.NODE_ENV === "development"
? err.message
: "Something went wrong",
},
500
);
};
// WSX error handler
wsx.onError = (error, connection) => {
console.error("WSX error:", error);
if (connection) {
connection.send(
JSON.stringify({
error: "An unexpected error occurred",
id: "error",
})
);
}
};
// Handle shutdown gracefully
process.on("SIGTERM", () => {
console.log("Received SIGTERM, shutting down gracefully");
// Close all WSX connections
const connections = wsx.getConnections();
connections.forEach((conn) => {
conn.send(
JSON.stringify({
type: "shutdown",
message: "Server is shutting down",
})
);
conn.close();
});
process.exit(0);
});
// Cache responses at the edge
const cache = new Map();
wsx.on("cached-data", async (request, connection) => {
const cacheKey = `${request.handler}:${JSON.stringify(request.data)}`;
// Check cache first
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
return {
...cached,
id: request.id,
};
}
// Generate response
const data = await getExpensiveData(request.data);
const response = {
target: request.target,
html: `<div>Data: ${JSON.stringify(data)}</div>`,
};
// Cache for 5 minutes
cache.set(cacheKey, response);
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000);
return {
id: request.id,
...response,
};
});
// Efficient connection management
class ConnectionPool {
constructor(maxConnections = 1000) {
this.connections = new Map();
this.maxConnections = maxConnections;
}
addConnection(connection) {
if (this.connections.size >= this.maxConnections) {
// Close oldest connection
const oldest = this.connections.keys().next().value;
const oldConnection = this.connections.get(oldest);
oldConnection.close();
this.connections.delete(oldest);
}
this.connections.set(connection.id, {
connection,
lastActivity: Date.now(),
});
}
updateActivity(connectionId) {
const conn = this.connections.get(connectionId);
if (conn) {
conn.lastActivity = Date.now();
}
}
cleanupInactive() {
const now = Date.now();
const timeout = 30 * 60 * 1000; // 30 minutes
for (const [id, conn] of this.connections) {
if (now - conn.lastActivity > timeout) {
conn.connection.close();
this.connections.delete(id);
}
}
}
}
const connectionPool = new ConnectionPool();
wsx.onConnection = (connection) => {
connectionPool.addConnection(connection);
};
// Cleanup inactive connections every 5 minutes
setInterval(() => {
connectionPool.cleanupInactive();
}, 5 * 60 * 1000);