Skip to main content

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. :::