SSR framework for Svelte 5 + Bun with islands-based selective hydration
On this page
WebSocket routes
Mochi.ws(handlers) registers a WebSocket endpoint backed by Bun’s ServerWebSocket. The handler map carries five callbacks — upgrade, open, message, close, drain — and exposes Bun’s pub/sub primitives (ws.subscribe, ws.publish, ws.unsubscribe) on the socket.
// file: src/index.ts
import { Mochi } from 'mochi-framework';
await Mochi.serve({
routes: {
'/ws/chat': Mochi.ws({
open(ws) {
ws.subscribe('chat');
},
message(ws, message) {
ws.publish('chat', String(message));
ws.send(String(message));
},
close(ws) {
ws.unsubscribe('chat');
},
}),
},
});Only message is required; the rest are optional.
upgrade
Runs once per HTTP upgrade request. Return a value to attach to ws.data.user, or return false to reject the connection. The route’s URL params are passed as the second argument.
// file: src/index.ts
import { Mochi } from 'mochi-framework';
await Mochi.serve({
routes: {
'/ws/:room': Mochi.ws<{ userId: string; room: string }>({
upgrade(req, params) {
const userId = req.headers.get('x-user-id');
if (!userId) return false; // reject the upgrade
return { userId, room: params.room };
},
message(ws, msg) {
console.log(ws.data.user.userId, ws.data.user.room, msg);
},
}),
},
});open
Fires once after a successful upgrade. Use it to subscribe the socket to topics or seed per-connection state.
open(ws) {
ws.subscribe('chat');
}message
Fires for every inbound frame. The payload is string | Buffer — coerce or decode it before use.
message(ws, message) {
ws.publish('chat', String(message));
}close
Fires once when the socket closes, with the close code and reason. Use it to release per-connection state and unsubscribe from topics.
close(ws, code, reason) {
ws.unsubscribe('chat');
}drain
Fires when the socket’s send buffer has drained after a backpressured ws.send. Resume queued writes here.
drain(ws) {
// resume buffered sends
}ws.data
Each socket carries a typed data object. Mochi reserves the internal fields __mochiRoutePattern, __mochiOpenedAt, and __mochiPath; your upgrade return value is exposed as ws.data.user.
Mochi.ws<{ userId: string }>({
upgrade(req) {
const userId = req.headers.get('x-user-id');
return userId ? { userId } : false;
},
message(ws) {
console.log(ws.data.user.userId);
},
});Pub/sub
Every socket exposes Bun’s pub/sub primitives: ws.subscribe(topic), ws.publish(topic, data), ws.unsubscribe(topic). To broadcast from outside a handler, capture the server returned by Mochi.serve() and call server.publish(topic, data).
open(ws) {
ws.subscribe('chat');
},
message(ws, msg) {
ws.publish('chat', String(msg)); // fan out to every other subscriber
},Lifecycle events
Every WebSocket emits ws:open, ws:message, and ws:close on mochiEvents. logger() already prints them. See Events for the full payload shape and how to add custom subscribers.
See it in action
Live demos showing key concepts from this page