Add entity CRUD events over WebSocket with auto-refresh
Broadcast entity_changed and device_health_changed events via the event bus so the frontend can auto-refresh cards without polling. Adds exponential backoff on WS reconnect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
61
server/src/wled_controller/static/js/core/entity-events.js
Normal file
61
server/src/wled_controller/static/js/core/entity-events.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Entity event listeners — reacts to server-pushed entity_changed and
|
||||
* device_health_changed WebSocket events by invalidating the relevant
|
||||
* DataCache and dispatching an `entity:reload` DOM event so active
|
||||
* feature modules can refresh their UI.
|
||||
*/
|
||||
|
||||
import {
|
||||
devicesCache, outputTargetsCache, colorStripSourcesCache,
|
||||
streamsCache, audioSourcesCache, valueSourcesCache,
|
||||
syncClocksCache, automationsCacheObj, scenePresetsCache,
|
||||
captureTemplatesCache, audioTemplatesCache, ppTemplatesCache,
|
||||
patternTemplatesCache,
|
||||
} from './state.js';
|
||||
|
||||
/** Maps entity_type string from the server event to its DataCache instance. */
|
||||
const ENTITY_CACHE_MAP = {
|
||||
device: devicesCache,
|
||||
output_target: outputTargetsCache,
|
||||
color_strip_source: colorStripSourcesCache,
|
||||
picture_source: streamsCache,
|
||||
audio_source: audioSourcesCache,
|
||||
value_source: valueSourcesCache,
|
||||
sync_clock: syncClocksCache,
|
||||
automation: automationsCacheObj,
|
||||
scene_preset: scenePresetsCache,
|
||||
capture_template: captureTemplatesCache,
|
||||
audio_template: audioTemplatesCache,
|
||||
pp_template: ppTemplatesCache,
|
||||
pattern_template: patternTemplatesCache,
|
||||
};
|
||||
|
||||
function _invalidateAndReload(entityType) {
|
||||
const cache = ENTITY_CACHE_MAP[entityType];
|
||||
if (cache) {
|
||||
cache.invalidate();
|
||||
cache.fetch();
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('entity:reload', {
|
||||
detail: { entity_type: entityType },
|
||||
}));
|
||||
}
|
||||
|
||||
function _onEntityChanged(e) {
|
||||
const { entity_type } = e.detail || {};
|
||||
if (!entity_type) return;
|
||||
_invalidateAndReload(entity_type);
|
||||
}
|
||||
|
||||
function _onDeviceHealthChanged() {
|
||||
_invalidateAndReload('device');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register listeners for server-pushed entity events.
|
||||
* Call once during app initialization, after startEventsWS().
|
||||
*/
|
||||
export function startEntityEventListeners() {
|
||||
document.addEventListener('server:entity_changed', _onEntityChanged);
|
||||
document.addEventListener('server:device_health_changed', _onDeviceHealthChanged);
|
||||
}
|
||||
@@ -2,13 +2,20 @@
|
||||
* Global events WebSocket — stays connected while logged in,
|
||||
* dispatches DOM custom events that feature modules can listen to.
|
||||
*
|
||||
* Events dispatched: server:state_change, server:automation_state_changed
|
||||
* Events dispatched:
|
||||
* server:state_change — target processing start/stop/crash
|
||||
* server:automation_state_changed — automation activated/deactivated
|
||||
* server:entity_changed — entity CRUD (create/update/delete)
|
||||
* server:device_health_changed — device online/offline status change
|
||||
*/
|
||||
|
||||
import { apiKey } from './state.js';
|
||||
|
||||
let _ws = null;
|
||||
let _reconnectTimer = null;
|
||||
let _reconnectDelay = 1000; // start at 1s, exponential backoff to 30s
|
||||
const _RECONNECT_MIN = 1000;
|
||||
const _RECONNECT_MAX = 30000;
|
||||
|
||||
export function startEventsWS() {
|
||||
stopEventsWS();
|
||||
@@ -19,6 +26,9 @@ export function startEventsWS() {
|
||||
|
||||
try {
|
||||
_ws = new WebSocket(url);
|
||||
_ws.onopen = () => {
|
||||
_reconnectDelay = _RECONNECT_MIN; // reset backoff on successful connection
|
||||
};
|
||||
_ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -27,7 +37,8 @@ export function startEventsWS() {
|
||||
};
|
||||
_ws.onclose = () => {
|
||||
_ws = null;
|
||||
_reconnectTimer = setTimeout(startEventsWS, 3000);
|
||||
_reconnectTimer = setTimeout(startEventsWS, _reconnectDelay);
|
||||
_reconnectDelay = Math.min(_reconnectDelay * 2, _RECONNECT_MAX);
|
||||
};
|
||||
_ws.onerror = () => {};
|
||||
} catch {
|
||||
@@ -45,4 +56,5 @@ export function stopEventsWS() {
|
||||
_ws.close();
|
||||
_ws = null;
|
||||
}
|
||||
_reconnectDelay = _RECONNECT_MIN;
|
||||
}
|
||||
|
||||
@@ -840,6 +840,13 @@ function _debouncedDashboardReload(forceFullRender = false) {
|
||||
|
||||
document.addEventListener('server:state_change', () => _debouncedDashboardReload());
|
||||
document.addEventListener('server:automation_state_changed', () => _debouncedDashboardReload(true));
|
||||
document.addEventListener('server:device_health_changed', () => _debouncedDashboardReload());
|
||||
|
||||
const _DASHBOARD_ENTITY_TYPES = new Set(['output_target', 'automation', 'scene_preset', 'sync_clock', 'device']);
|
||||
document.addEventListener('server:entity_changed', (e) => {
|
||||
const { entity_type } = e.detail || {};
|
||||
if (_DASHBOARD_ENTITY_TYPES.has(entity_type)) _debouncedDashboardReload(true);
|
||||
});
|
||||
|
||||
// Re-render dashboard when language changes
|
||||
document.addEventListener('languageChanged', () => {
|
||||
|
||||
Reference in New Issue
Block a user