v0.21

websockets

introduction

dframework ships with a fully integrated socket layer built on top of the ws library. it provides a dedicated event router that mirrors the http Route architecture, complete with middleware support, controller resolution, session authentication, and html rendering. the socket server boots automatically alongside your http server. there is nothing to install separately.

on the client side, the framework's frontend runtime exposes a global Socket facade that manages the connection, event emission, and event listening. the socket connection is established automatically when the spa router initializes.

for information on d-wire and d-live reactive elements that build on top of this socket layer, see the reactivity documentation.

how it works

when your application starts, the framework attaches a socket upgrade handler to the http server on the /ws path. when a browser opens a websocket connection, the framework creates a full Request object from the upgrade request, giving you access to cookies, sessions, headers, and the authenticated user inside your socket event handlers.

incoming messages are expected as json objects with an event property. the socket router looks up the registered handler for that event name and runs it through the middleware pipeline, exactly as an http route would be processed.

defining socket events

socket events are defined in your route files, typically routes/wire.js. you register events using the global Socket facade, which delegates to the underlying SocketRouter instance.

1import { Socket } from 'dframework';

event handlers

the Socket.on() method registers a handler for a named event. the event name uses colon separated namespacing by convention.

1Socket.on('chat:message', (req) => {
2 const { text } = req.body;
3 return json({ received: true, text });
4});

the handler receives the same arguments as an http route handler. req contains the full request object with access to req.body (the event payload), req.user, req.session, req.cookies, and all standard request methods. the global response helpers (render(), json(), redirect(), abort(), cookie()) work inside socket handlers exactly as they do in http controllers.

if the handler returns a value without calling any response method, the framework automatically wraps it in a json response with a 200 status code.

inline handlers

for simple events, you can use an inline function directly.

1Socket.on('ping', () => {
2 return json({ pong: true, time: Date.now() });
3});

controller handlers

for complex logic, reference a controller method using the familiar Controller@method string syntax. the framework resolves and imports the controller dynamically.

1Socket.on('chat:message', 'ChatController@onMessage');
2Socket.on('chat:typing', 'ChatController@onTyping');
1export default class ChatController {
2 async onMessage(req) {
3 const { text } = req.body;
4 await Message.create({ user_id: req.user.id, text });
5 Socket.broadcast('chat:new', { text, user: req.user.name });
6 }
7
8 async onTyping() {
9 return json({ typing: true });
10 }
11}

socket wire controllers are stored in the same controllers directory as http controllers and follow the same conventions. a single controller can handle both http routes and socket events.

middleware

socket events support the same middleware system as http routes. middleware functions receive req and next, and must call next() to proceed to the handler.

inline middleware

you can attach middleware to a single event by passing an array where the last element is the handler and all preceding elements are middleware references.

1Socket.on('admin:action', ['AuthMiddleware@requireAuth', 'AdminController@execute']);

grouped middleware

the Socket.group() method applies shared middleware to multiple events at once.

1Socket.group({ middleware: 'AuthMiddleware@requireAuth' }, (auth) => {
2 auth.on('profile:update', 'ProfileController@update');
3 auth.on('profile:avatar', 'ProfileController@avatar');
4});

you can also chain middleware using the Socket.middleware() method for a more fluent api.

1Socket.middleware('AuthMiddleware@requireAuth').group({}, (auth) => {
2 auth.on('dashboard:refresh', 'DashboardController@refresh');
3});

here is how socket events look in a real application:

1import { Socket } from 'dframework';
2
3Socket.on('pair:status', 'auth.AuthController@wire_status').targets(['#qr-box']);
4
5Socket.group({ middleware: ['AuthMiddleware@requireAuth'] }, (auth) => {
6 auth.on('follow:toggle', 'app.FriendsController@toggle').targets(['#leaderboard-list', '#friends-list-container']);
7 auth.on('friends:tab', 'app.FriendsController@friends_tab').targets(['#friends-list-container']);
8 auth.on('playback:heartbeat', 'app.PlaybackController@handleHeartbeat');
9});

responding to events

socket handlers use the same global response helpers available in http controllers. all responses are delivered over the socket connection.

rendering html

the render() helper compiles a view template and sends the resulting html to the client. the framework morphs the html into the specified target element on the client side without a page refresh.

1Socket.on('feed:refresh', async () => {
2 const posts = await Post.latest(20);
3 return render('partials.feed', { posts });
4});

the rendered html is sent as a d-wire:render event. the client side runtime parses it and morphs the target dom element, preserving focus state, input values, and scroll positions.

the framework uses payload hashing to prevent redundant dom updates. if the rendered html produces the same hash as the previous render for that target, the update is silently skipped.

sending json

the json() helper sends a json payload back to the client.

1Socket.on('stats:fetch', async () => {
2 const count = await User.count();
3 return json({ users: count });
4});

redirects

the redirect() helper sends a redirect instruction to the client. the browser will navigate to the specified url.

1Socket.on('session:expired', () => {
2 return redirect('/login');
3});

aborting

the abort() helper sends an error response with a status code and optional message.

1Socket.on('admin:action', (req) => {
2 if (!req.user?.isAdmin) {
3 return abort(403, 'forbidden');
4 }
5});

the default status messages are 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, and 500 Internal Error.

cookies

you can set cookies from a socket handler using the cookie() helper. the cookies are sent alongside the response and the client side runtime applies them to the browser.

1Socket.on('theme:set', (req) => {
2 cookie('theme', req.body.theme, { path: '/', sameSite: 'Lax' });
3 return json({ ok: true });
4});

target restrictions

you can restrict which dom target selectors an event is allowed to render into using the targets() chain method. if the client sends a target that is not in the allowed list, the event is silently rejected.

1Socket.on('sidebar:update', 'SidebarController@render').targets('#sidebar');
2Socket.on('panel:refresh', 'PanelController@render').targets(['#left-panel', '#right-panel']);

broadcasting

the Socket.broadcast() method sends an event to all connected websocket clients. you can optionally exclude a specific connection (typically the sender).

1Socket.on('chat:send', (req, res, ws) => {
2 const message = { text: req.body.text, user: req.user.name };
3 Socket.broadcast('chat:new', message, ws);
4 return json({ sent: true });
5});

the third argument is the websocket connection to exclude from the broadcast. passing the current ws prevents the sender from receiving their own message.

you can also call Socket.broadcast() and Socket.setState() directly from background jobs in jobs/. worker threads automatically relay socket events to the main process without requiring external brokers like redis.

1// inside any background job
2Socket.broadcast('task:complete', { taskId: 42 });
3Socket.setState('#task-status', { done: true });

targeted delivery

when you need to send messages to specific clients instead of broadcasting to everyone, dframework provides targeted methods on the Socket facade. connections automatically track authenticated user identities and session ids upon connection and during authenticated socket requests.

sending to a specific user

the Socket.toUser() method sends an event to all open websocket connections belonging to a user id or user model instance.

1// using numeric user id
2Socket.toUser(42, 'notification:new', { text: 'you received a payment' });
3
4// using model instance
5Socket.toUser(user, 'notification:new', { text: 'profile updated' });

you can also target component state updates to a specific user:

1Socket.toUser(42).setState('#cart-count', { count: 3 });

sending to multiple users

the Socket.toUsers() method dispatches an event to a list of user ids or user model instances:

1Socket.toUsers([1, 2, 3], 'room:update', { active: true });

sending to a session

the Socket.toSession() method targets all connections matching a specific session id:

1Socket.toSession(sessionId, 'session:revoked', { reason: 'logged in from new device' });

fluent targeting

the Socket.to() helper offers flexible targeting for websocket instances, user ids, arrays, or session strings:

1// target websocket instance directly
2Socket.to(ws).emit('direct:message', { ok: true });
3
4// target by user id or session
5Socket.to(42).emit('user:alert', { priority: 'high' });
6Socket.to('session-token-xyz').emit('session:ping', {});

multi guard targeting

if your application defines multiple authentication guards in config/auth.js (for example web, admin, partner), connections automatically track identities across all configured guards.

Socket.toUser(id) delivers to connections matching that user id on any guard (or the default guard). to target a specific guard explicitly, scope the dispatch using Socket.guard():

1// target user 5 specifically on the admin guard
2Socket.guard('admin').toUser(5, 'admin:alert', { text: 'server restart scheduled' });
3
4// fluent chaining on custom guard
5Socket.guard('admin').toUser(5).setState('#admin-panel', { status: 'maintenance' });

all targeted methods (toUser, toUsers, toSession, sendTo, guard) work seamlessly across http controllers, wire handlers, and background jobs.

the socket facade

the Socket global is a proxy facade that delegates all method calls to the application's SocketRouter instance. it is available globally after the application initializes and can be imported from the framework.

1import { Socket } from 'dframework';

the facade exposes all methods: on(), group(), middleware(), broadcast(), setState(), toUser(), toUsers(), toSession(), sendTo(), to(), and liveNotify().

client side usage

the framework's frontend runtime provides a global Socket object on the window that manages the websocket connection.

connecting

the websocket connection is established automatically when the spa router initializes. you can also connect manually.

1await Socket.connect();

the connection url defaults to /ws and automatically uses wss: on secure pages and ws: on insecure ones. the full uri is constructed from the current page's location.host.

the Socket frontend facade is only available inside scoped scripts (type="text/dspa", inline page scripts, and module scripts with d-spa-scope). it is not available outside of scope contexts.

emitting events

use Socket.emit() to send an event to the server. you can pass a data payload and an optional target selector.

1Socket.emit('chat:send', { text: 'hello world' });
1Socket.emit('dashboard:refresh', { filter: 'today' }, '#dashboard');

when a target is specified, the server handler's rendered output will be morphed into that specific dom element.

listening for events

use Socket.on() on the client side to listen for events pushed from the server.

1Socket.on('chat:new', (data) => {
2 appendMessage(data.text, data.user);
3});

for broadcast events, the callback receives the data payload directly.

origin protection

IMPORTANT

the socket server validates the `Origin` header of every upgrade request against `APP_URL`. connections from mismatched origins are immediately rejected to prevent cross site websocket hijacking (cswsh) attacks.

in the local environment, connections from localhost and 127.0.0.1 are allowed regardless of the configured url.

debug mode

in the local environment with debug mode enabled, the socket router attaches detailed timing and query information to every response. the debugbar on the client side displays the elapsed time, executed queries, and request headers for each socket event, providing the same level of visibility as http requests.