WebSocket API
The WebSocket connection (Socket.io) is GEM's primary real-time channel — state, subscriptions, commands, and most interactive operations flow over it. The browser/mobile clients talk to it through the GemApp class; server handlers live in gem_client.js.
Calling from a client
GemApp wraps each socket event as a promise-returning method:
const gem = GemApp.getInstance();
// Data
await gem.query('zone', { subsystem_id: 11 });
await gem.queryOne('device', { id: 1 });
await gem.insertModel('model', data);
await gem.updateModel('model', id, updates);
await gem.deleteModel('model', id);
// Attributes
await gem.getAttributes('zone', zoneId);
await gem.getAttribute('device', deviceId, 'ip_address');
await gem.setAttribute('zone', zoneId, 'brightness', 50, 'integer');
// Commands & macros
await gem.command({ device: deviceId, zone: zoneId, action: 'on', level: 100 });
await gem.macro(macroId);
await gem.reload('device', deviceId);
// Real-time subscriptions
const token = await gem.subscribe('zone', zoneId, callback);
await gem.unsubscribe('zone', zoneId, token);
Subscriptions are how a UI stays live: register a callback for an entity and GEM pushes changes as they happen, rather than the client polling.
Permissions are automatic
Every socket.on(event, …) handler is auto-enumerated as a permissionable api action (via socket.eventNames()), so access is controlled by role rules without a separate registry — see RBAC. This is the same surface the REST API is gated against.
Adding a server function
A new operation is two additions (existing names are never repurposed — the socket surface is a frozen contract):
1. Backend — in gem_client.js, inside setHandlers(), before the auth middleware:
socket.on('my_function', async (params, cb) => {
try {
const result = await doSomething(params);
if (typeof cb === 'function') cb(result);
} catch (error) {
console.error('my function error:', error);
if (typeof cb === 'function') cb({ error: error.message });
}
});
2. Frontend — add the matching method to GemApp:
myFunction(params) {
let self = this;
return new Promise((resolve, reject) => {
self.socket.emit('my_function', params, (result) => {
if (result && result.error) reject(new Error(result.error));
else resolve(result);
});
});
}
:::warning Always guard the callback
Clients may emit without a callback. Guard with typeof cb === 'function' before calling it — an unguarded cb() can crash the handler. Adding the new event automatically makes it available to assign in roles.
:::