@zolomedia/bifrost-client 1.7.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- package/zSys/zSys.js +14 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ═══════════════════════════════════════════════════════════════
|
|
3
|
+
* Message Handler Module - Message Processing & Correlation
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* @module core/message_handler
|
|
7
|
+
* @layer 4 (Event Handlers)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Constants
|
|
11
|
+
import { TIMEOUTS, PROTOCOL_EVENTS, PROTOCOL_REASONS } from '../../L1_Foundation/constants/bifrost_constants.js';
|
|
12
|
+
|
|
13
|
+
// zRender op-code decoder — reverse map of render_opcodes.py EVENT_TO_OP
|
|
14
|
+
// (op → display handler key). This table contains no business logic, wizard
|
|
15
|
+
// flows, or application routes — only the wire-op ⇄ display-event vocabulary.
|
|
16
|
+
//
|
|
17
|
+
// DRIFT WARNING: this is a hand-maintained mirror of the server SSOT at
|
|
18
|
+
// zGuard/zguard/bifrost/zBifrost_modules/render/render_opcodes.py. Keep the
|
|
19
|
+
// entry count in sync (currently 35). If the server adds/renames an op, an
|
|
20
|
+
// unknown opcode will surface via _warnUnknownOpcode() below instead of being
|
|
21
|
+
// silently dropped.
|
|
22
|
+
const _ZRENDER_OPS = {"tx":"text","hd":"header","im":"image","rt":"rich_text","ic":"icon","zu":"zURL","zt":"zTable","er":"error","wr":"warning","su":"success","inf":"info","rs":"read_string","pb":"progress_bar","mn":"zMenu","zd":"zDash","zdl":"zDialog","zi":"zInput","sep":"separator","cod":"code","lnk":"link","bdg":"badge","spn":"spinner","ls":"list","dl":"dl","btn":"button","rb":"read_bool","rp":"read_password","jsn":"json","div":"divider","crd":"card","ztrm":"zTerminal","sel":"selection","zcr":"zCrumbs","pc":"progress_complete","swi":"swiper_init"};
|
|
23
|
+
|
|
24
|
+
// Surface opcode-mirror drift loudly (once per unknown op) so a server change
|
|
25
|
+
// that this client hasn't mirrored is visible instead of silently swallowed.
|
|
26
|
+
const _warnedOpcodes = new Set();
|
|
27
|
+
function _warnUnknownOpcode(op) {
|
|
28
|
+
if (_warnedOpcodes.has(op)) return;
|
|
29
|
+
_warnedOpcodes.add(op);
|
|
30
|
+
console.warn(
|
|
31
|
+
`[zRender] Unknown render opcode "${op}" — client opcode map is out of sync ` +
|
|
32
|
+
`with the server (render_opcodes.py). Node passed through undecoded.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _decodeRenderNode(node) {
|
|
37
|
+
if (!node || typeof node !== 'object') return node;
|
|
38
|
+
if (Array.isArray(node)) return node.map(_decodeRenderNode);
|
|
39
|
+
// Node carrying an op code — decode known ops; flag unknown ones as drift.
|
|
40
|
+
if ('e' in node && typeof node.e === 'string') {
|
|
41
|
+
if (_ZRENDER_OPS[node.e]) {
|
|
42
|
+
const decoded = { event: _ZRENDER_OPS[node.e] };
|
|
43
|
+
for (const [k, v] of Object.entries(node)) {
|
|
44
|
+
if (k === 'e') continue;
|
|
45
|
+
decoded[k] = _decodeRenderNode(v);
|
|
46
|
+
}
|
|
47
|
+
return decoded;
|
|
48
|
+
}
|
|
49
|
+
_warnUnknownOpcode(node.e);
|
|
50
|
+
}
|
|
51
|
+
// Container / metadata node (or undecodable op) — recurse into all values.
|
|
52
|
+
const decoded = {};
|
|
53
|
+
for (const [k, v] of Object.entries(node)) {
|
|
54
|
+
decoded[k] = _decodeRenderNode(v);
|
|
55
|
+
}
|
|
56
|
+
return decoded;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class MessageHandler {
|
|
60
|
+
constructor(logger, hooks, client = null) {
|
|
61
|
+
this.logger = logger;
|
|
62
|
+
this.hooks = hooks;
|
|
63
|
+
this.client = client; // Store reference to BifrostClient for client-side navigation
|
|
64
|
+
|
|
65
|
+
// Pass logger to hooks for better error handling
|
|
66
|
+
if (this.hooks && typeof this.hooks.logger === 'undefined') {
|
|
67
|
+
this.hooks.logger = logger;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.requestId = 0;
|
|
71
|
+
this.callbacks = new Map();
|
|
72
|
+
this.timeout = TIMEOUTS.REQUEST_TIMEOUT; // Default timeout from constants
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set timeout for requests
|
|
77
|
+
*/
|
|
78
|
+
setTimeout(timeout) {
|
|
79
|
+
this.timeout = timeout;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract session ID from HTTP cookie for session sync (WebSocket/HTTP bridge).
|
|
84
|
+
*
|
|
85
|
+
* SECURITY NOTE: this reads `session`/`sessionid` via document.cookie, which only
|
|
86
|
+
* works when the cookie is NOT HttpOnly. The authoritative session lives in the
|
|
87
|
+
* server-side store; this value is a best-effort sync hint and MUST NOT be treated
|
|
88
|
+
* as proof of identity (the server re-validates). Deployments that set HttpOnly
|
|
89
|
+
* (recommended) will simply get null here and rely on the browser to attach the
|
|
90
|
+
* cookie to the WS handshake — that is the safer path.
|
|
91
|
+
*
|
|
92
|
+
* @private
|
|
93
|
+
* @returns {string|null} Session ID or null if not found / HttpOnly
|
|
94
|
+
*/
|
|
95
|
+
_getSessionIdFromCookie() {
|
|
96
|
+
// Parse all cookies
|
|
97
|
+
const cookies = document.cookie.split(';');
|
|
98
|
+
|
|
99
|
+
// Look for 'session' cookie (Flask default) or 'sessionid' (Django)
|
|
100
|
+
for (const cookie of cookies) {
|
|
101
|
+
const [name, value] = cookie.trim().split('=');
|
|
102
|
+
if (name === 'session' || name === 'sessionid') {
|
|
103
|
+
this.logger.log(`[MessageHandler] Found session cookie: ${name}=${value.substring(0, 10)}...`);
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.logger.log('[MessageHandler] [WARN] No session cookie found (user not logged in)');
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate outgoing message follows protocol
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
_validateOutgoingMessage(payload) {
|
|
117
|
+
// Warn if using deprecated 'action' field
|
|
118
|
+
if (payload.action && !payload.event) {
|
|
119
|
+
this.logger.warn(
|
|
120
|
+
'Using deprecated "action" field. Please use "event" instead.',
|
|
121
|
+
{ action: payload.action }
|
|
122
|
+
);
|
|
123
|
+
// Auto-migrate: copy action to event
|
|
124
|
+
payload.event = payload.action;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Warn if message has both 'action' and 'event'
|
|
128
|
+
if (payload.action && payload.event && payload.action !== payload.event) {
|
|
129
|
+
this.logger.warn(
|
|
130
|
+
'Message has both "action" and "event" fields with different values. Using "event".',
|
|
131
|
+
{ action: payload.action, event: payload.event }
|
|
132
|
+
);
|
|
133
|
+
delete payload.action;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle incoming message
|
|
139
|
+
*/
|
|
140
|
+
handleMessage(data) {
|
|
141
|
+
try {
|
|
142
|
+
const message = JSON.parse(data);
|
|
143
|
+
this.logger.debug('[MessageHandler] Received message:', message.event || message.display_event || 'unknown');
|
|
144
|
+
|
|
145
|
+
// Debug zDialog messages to see full structure
|
|
146
|
+
if (message.display_event === PROTOCOL_EVENTS.ZDIALOG || (message.data && message.data.event === PROTOCOL_EVENTS.ZDIALOG)) {
|
|
147
|
+
this.logger.log('[MessageHandler] zDialog message structure:', JSON.stringify(message, null, 2));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Call general message hook (with error boundary)
|
|
151
|
+
try {
|
|
152
|
+
this.hooks.call('onMessage', message);
|
|
153
|
+
} catch (hookError) {
|
|
154
|
+
this.logger.error('[MessageHandler] Error in onMessage hook:', hookError);
|
|
155
|
+
// Continue processing - don't let hook errors break message handling
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Progressive chunk rendering (zWizard chunked execution for Bifrost)
|
|
159
|
+
// MUST be checked BEFORE response correlation (chunks have _requestId but are NOT responses)
|
|
160
|
+
if (message.event === PROTOCOL_EVENTS.RENDER_CHUNK) {
|
|
161
|
+
this.logger.debug('[MessageHandler] Chunk event detected:', message.chunk_num);
|
|
162
|
+
// Decode zRender op codes back to display event names before handing to renderers
|
|
163
|
+
if (message.data) {
|
|
164
|
+
message.data = _decodeRenderNode(message.data);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
this.hooks.call('onRenderChunk', message);
|
|
168
|
+
} catch (hookError) {
|
|
169
|
+
this.logger.error('[MessageHandler] Error rendering chunk:', hookError);
|
|
170
|
+
this.hooks.call('onError', { type: 'chunk_render_error', error: hookError, message });
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Connection info event (session data from backend) - v1.6.0
|
|
176
|
+
if (message.event === PROTOCOL_EVENTS.CONNECTION_INFO) {
|
|
177
|
+
this.logger.debug('[MessageHandler] Connection info detected');
|
|
178
|
+
this.hooks.call('onConnectionInfo', message.data);
|
|
179
|
+
// Also trigger onConnected for backward compatibility
|
|
180
|
+
this.hooks.call('onConnected', message.data);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Error event from backend (walker execution errors, validation errors, etc.)
|
|
185
|
+
if (message.event === PROTOCOL_EVENTS.ERROR) {
|
|
186
|
+
this.logger.error('[MessageHandler] Backend error received:', message.message || message.error);
|
|
187
|
+
|
|
188
|
+
// Clear navigation timeout if active
|
|
189
|
+
if (this.client && this.client._navigationTimeout) {
|
|
190
|
+
clearTimeout(this.client._navigationTimeout);
|
|
191
|
+
this.client._navigationTimeout = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Show error in content area
|
|
195
|
+
if (this.client && this.client._zVaFElement) {
|
|
196
|
+
this.client._zVaFElement.innerHTML = `
|
|
197
|
+
<div class="zAlert zAlert-danger zmt-4">
|
|
198
|
+
<strong>Backend Error:</strong> ${message.message || message.error || 'Unknown error'}
|
|
199
|
+
${message.details ? `<br><small>${message.details}</small>` : ''}
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Call error hook
|
|
205
|
+
this.hooks.call('onError', message);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Navigate back event (^ bounce-back after block completion)
|
|
210
|
+
if (message.event === PROTOCOL_EVENTS.NAVIGATE_BACK) {
|
|
211
|
+
this.logger.log('[MessageHandler] NAVIGATE_BACK EVENT - triggering browser back');
|
|
212
|
+
this.logger.log('[MessageHandler] Reason:', message.reason);
|
|
213
|
+
|
|
214
|
+
// For bounce-back completions (e.g., after login/logout), always navigate to home via client-side nav
|
|
215
|
+
// This avoids double-back issues and ensures correct block loading
|
|
216
|
+
if (message.reason === PROTOCOL_REASONS.BOUNCE_BACK_COMPLETED) {
|
|
217
|
+
// Refresh NavBar after any bounce (RBAC updates after login/logout).
|
|
218
|
+
const refreshNav = () => {
|
|
219
|
+
if (typeof this.client._fetchAndPopulateNavBar === 'function') {
|
|
220
|
+
this.logger.log('[MessageHandler] Refreshing NavBar after bounce-back');
|
|
221
|
+
this.client._fetchAndPopulateNavBar().catch(err => {
|
|
222
|
+
this.logger.error('[MessageHandler] Failed to refresh NavBar:', err);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// ^ bounce block (no explicit target): return to the previous page.
|
|
228
|
+
if (message.back) {
|
|
229
|
+
this.logger.log('[MessageHandler] Bounce-back - returning to previous page');
|
|
230
|
+
window.history.back();
|
|
231
|
+
// History navigation re-renders asynchronously; refresh navbar after it settles.
|
|
232
|
+
setTimeout(refreshNav, 300);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Explicit (onSuccess) target, else home for plain bounces.
|
|
237
|
+
const target = message.url || '/';
|
|
238
|
+
this.logger.log('[MessageHandler] Bounce-back - navigating to ' + target + ' via client-side nav');
|
|
239
|
+
if (this.client && typeof this.client._navigateToRoute === 'function') {
|
|
240
|
+
this.client._navigateToRoute(target).then(refreshNav).catch(err => {
|
|
241
|
+
this.logger.error('[MessageHandler] Navigation failed:', err);
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
// Fallback: use window.location (will cause reload)
|
|
245
|
+
window.location.href = target;
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// For RBAC denials, prefer an explicit redirect target (SSOT: zRBAC
|
|
251
|
+
// onDenied / global login route resolved server-side). Falls back to
|
|
252
|
+
// history.back()/home when no target was provided.
|
|
253
|
+
if (message.reason === PROTOCOL_REASONS.RBAC_DENIED) {
|
|
254
|
+
if (message.url) {
|
|
255
|
+
this.logger.log('[MessageHandler] RBAC denied - redirecting to ' + message.url);
|
|
256
|
+
if (this.client && typeof this.client._navigateToRoute === 'function') {
|
|
257
|
+
this.client._navigateToRoute(message.url).then(() => {
|
|
258
|
+
if (typeof this.client._fetchAndPopulateNavBar === 'function') {
|
|
259
|
+
this.client._fetchAndPopulateNavBar().catch(() => {});
|
|
260
|
+
}
|
|
261
|
+
}).catch(err => {
|
|
262
|
+
this.logger.error('[MessageHandler] RBAC redirect failed:', err);
|
|
263
|
+
window.location.href = message.url;
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
window.location.href = message.url;
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hasAppHistory = window.history.length > 2 ||
|
|
272
|
+
(window.history.length > 1 && document.referrer.includes(window.location.hostname));
|
|
273
|
+
|
|
274
|
+
if (hasAppHistory) {
|
|
275
|
+
this.logger.log('[MessageHandler] RBAC denied - using history.back()');
|
|
276
|
+
window.history.back();
|
|
277
|
+
} else {
|
|
278
|
+
this.logger.log('[MessageHandler] RBAC denied, no history - navigating to home');
|
|
279
|
+
if (this.client && typeof this.client._navigateToRoute === 'function') {
|
|
280
|
+
this.client._navigateToRoute('/');
|
|
281
|
+
} else {
|
|
282
|
+
window.location.href = '/';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Default: attempt history.back() for other navigate_back reasons
|
|
289
|
+
this.logger.log('[MessageHandler] Other reason - using history.back()');
|
|
290
|
+
window.history.back();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Wizard gate result (post-gate steps from wizard_gate_submit)
|
|
295
|
+
if (message.event === PROTOCOL_EVENTS.WIZARD_GATE_RESULT) {
|
|
296
|
+
this.logger.debug('[MessageHandler] wizard_gate_result received for gate:', message.gateKey);
|
|
297
|
+
this.hooks.call('onWizardGateResult', message);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Dashboard event (zDash display event for sidebar navigation)
|
|
302
|
+
if (message.event === PROTOCOL_EVENTS.ZDASH) {
|
|
303
|
+
this.logger.debug('[MessageHandler] zDash event detected');
|
|
304
|
+
this.logger.log(' [MessageHandler] Dashboard config:', message);
|
|
305
|
+
this.hooks.call('onZDash', message);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Menu event (menu navigation in Bifrost mode)
|
|
310
|
+
// Note: Backend sends 'zMenu' not 'menu' (matches zDash, zDialog pattern)
|
|
311
|
+
if (message.event === PROTOCOL_EVENTS.ZMENU) {
|
|
312
|
+
this.logger.debug('[MessageHandler] zMenu event detected');
|
|
313
|
+
this.logger.log(' [MessageHandler] Menu config:', message);
|
|
314
|
+
this.hooks.call('onMenu', message);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// RBAC denial event (access denied)
|
|
319
|
+
if (message.event === PROTOCOL_EVENTS.RBAC_DENIED) {
|
|
320
|
+
this.logger.log(' [MessageHandler] RBAC ACCESS DENIED');
|
|
321
|
+
this.logger.log(' RBAC Access Denied:', message.message);
|
|
322
|
+
|
|
323
|
+
// Display the denial message
|
|
324
|
+
if (message.message) {
|
|
325
|
+
// Create a styled error display using zTheme classes
|
|
326
|
+
const errorDiv = document.createElement('div');
|
|
327
|
+
errorDiv.className = 'zAlert zAlert-danger zmt-4 zp-4';
|
|
328
|
+
errorDiv.innerHTML = `
|
|
329
|
+
<h3 class="zAlert-heading zmb-2"> Access Denied</h3>
|
|
330
|
+
<div class="zAlert-body">${message.message.replace(/\n/g, '<br>')}</div>
|
|
331
|
+
<hr class="zmy-3">
|
|
332
|
+
<p class="zmb-0 zText-muted"><small>You will be redirected back in a moment...</small></p>
|
|
333
|
+
`;
|
|
334
|
+
|
|
335
|
+
// Clear content area and show error
|
|
336
|
+
const contentArea = document.getElementById('zVaF-content');
|
|
337
|
+
if (contentArea) {
|
|
338
|
+
contentArea.innerHTML = ''; // Clear blank content
|
|
339
|
+
contentArea.appendChild(errorDiv);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check if this is a response to a request
|
|
347
|
+
const requestId = message._requestId;
|
|
348
|
+
if (requestId !== undefined && this.callbacks.has(requestId)) {
|
|
349
|
+
this._handleResponse(requestId, message);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// If a message looks like a BARE request/response (no event type) but carries
|
|
354
|
+
// no _requestId while callbacks are pending, that's a real backend protocol bug.
|
|
355
|
+
// Event-typed messages (execute_zfunc_response, execute_code_response,
|
|
356
|
+
// input_response, …) legitimately carry `result` and route by their own
|
|
357
|
+
// `event`/`requestId` below — they must NOT trip this guard or it false-fires
|
|
358
|
+
// on every avatar zInja (execute_zfunc) render.
|
|
359
|
+
if (!message.event &&
|
|
360
|
+
(message.result !== undefined || message.error !== undefined) &&
|
|
361
|
+
this.callbacks.size > 0) {
|
|
362
|
+
this.logger.error(
|
|
363
|
+
'Received response without _requestId! Backend must echo _requestId in all responses.',
|
|
364
|
+
{ message, pendingRequests: this.callbacks.size }
|
|
365
|
+
);
|
|
366
|
+
// Don't try to correlate - this is a backend bug that must be fixed
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check for specific event types
|
|
370
|
+
if (message.event === PROTOCOL_EVENTS.INPUT_RESPONSE) {
|
|
371
|
+
return; // Handled internally by zDisplay
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle execute_zfunc_response — resolves promise in ZDisplayOrchestrator._executeZFunc
|
|
375
|
+
if (message.event === PROTOCOL_EVENTS.EXECUTE_ZFUNC_RESPONSE) {
|
|
376
|
+
this.logger.log('[MessageHandler] execute_zfunc_response received:', message.requestId);
|
|
377
|
+
this.hooks.call('onZFuncResponse', message);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Handle execute_code_response for zTerminal
|
|
382
|
+
if (message.event === PROTOCOL_EVENTS.EXECUTE_CODE_RESPONSE) {
|
|
383
|
+
this.logger.log('[MessageHandler] execute_code_response received:', message.requestId);
|
|
384
|
+
// Route to TerminalRenderer's static handler
|
|
385
|
+
const TerminalRenderer = window._TerminalRenderer;
|
|
386
|
+
if (TerminalRenderer && TerminalRenderer.handleExecutionResponse) {
|
|
387
|
+
TerminalRenderer.handleExecutionResponse(message.requestId, message);
|
|
388
|
+
} else if (window._zTerminalResponses && window._zTerminalResponses[message.requestId]) {
|
|
389
|
+
// Fallback to direct promise resolution
|
|
390
|
+
window._zTerminalResponses[message.requestId](message);
|
|
391
|
+
delete window._zTerminalResponses[message.requestId];
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle request_input for zTerminal interactive input
|
|
397
|
+
if (message.event === PROTOCOL_EVENTS.REQUEST_INPUT) {
|
|
398
|
+
this.logger.log('[MessageHandler] request_input received:', message.requestId, message.prompt, 'isPassword:', message.isPassword);
|
|
399
|
+
|
|
400
|
+
// zFunc execution: route to inline widget rendered by ZDisplayOrchestrator
|
|
401
|
+
if (message.zfuncRequestId) {
|
|
402
|
+
this.hooks.call('onZFuncInput', message);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Route to TerminalRenderer's static handler
|
|
407
|
+
const TerminalRenderer = window._TerminalRenderer;
|
|
408
|
+
if (TerminalRenderer && TerminalRenderer.handleInputRequest) {
|
|
409
|
+
TerminalRenderer.handleInputRequest(
|
|
410
|
+
message.requestId,
|
|
411
|
+
message.prompt,
|
|
412
|
+
message.inputType || 'text',
|
|
413
|
+
message.required || false,
|
|
414
|
+
message.isPassword || false,
|
|
415
|
+
message.defaultValue || '',
|
|
416
|
+
message.isReadonly || false,
|
|
417
|
+
message.isDisabled || false,
|
|
418
|
+
message.placeholder || '',
|
|
419
|
+
message.datalist || [],
|
|
420
|
+
message.min ?? null,
|
|
421
|
+
message.max ?? null,
|
|
422
|
+
message.step ?? null,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Open a served resource in a new browser tab. Emitted by the server when
|
|
429
|
+
// a zTerminal swap-run (zOrigin=zBifrost) delegates an 'open' to the client
|
|
430
|
+
// instead of launching on the server (TRUST #35 — server never opens GUIs
|
|
431
|
+
// for remote visitors; the open happens in THIS browser).
|
|
432
|
+
if (message.event === PROTOCOL_EVENTS.OPEN_URL) {
|
|
433
|
+
if (message.url) {
|
|
434
|
+
this.logger.log('[MessageHandler] open_url received — opening new tab:', message.url);
|
|
435
|
+
window.open(message.url, '_blank', 'noopener,noreferrer');
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Walker finished a fire-and-forget render (navigation). When the server
|
|
441
|
+
// attaches a zPsi anchor — a crumb-driven zBack that should land on the
|
|
442
|
+
// section the user navigated FROM — scroll that section into view once the
|
|
443
|
+
// streamed chunks have painted. SSOT: the section comes from zCrumbs on the
|
|
444
|
+
// server; the client only honours the anchor it is handed.
|
|
445
|
+
if (message.event === 'walker_complete') {
|
|
446
|
+
if (message.zPsi) {
|
|
447
|
+
const anchor = String(message.zPsi);
|
|
448
|
+
const root = (this.client && this.client._zVaFElement) || document;
|
|
449
|
+
// Chunks render asynchronously (awaited renderItems, emoji/icon loads),
|
|
450
|
+
// so the target section may not exist the instant walker_complete lands.
|
|
451
|
+
// Poll briefly until it paints, then scroll. data-zkey is stamped on
|
|
452
|
+
// every top-level section; _zId is the fallback for anchored blocks.
|
|
453
|
+
let tries = 0;
|
|
454
|
+
const maxTries = 30; // ~2.4s at 80ms
|
|
455
|
+
const tryScroll = () => {
|
|
456
|
+
const target = root.querySelector(`[data-zkey="${anchor}"]`)
|
|
457
|
+
|| document.getElementById(anchor);
|
|
458
|
+
if (target) {
|
|
459
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
460
|
+
this.logger.log('[MessageHandler] zBack zPsi → scrolled to section:', anchor);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (++tries < maxTries) {
|
|
464
|
+
setTimeout(tryScroll, 80);
|
|
465
|
+
} else {
|
|
466
|
+
this.logger.warn('[MessageHandler] zBack zPsi → section not found after retries:', anchor);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
requestAnimationFrame(tryScroll);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Handle real-time output lines from execute_code execution (e.g. zText / Show_Result steps)
|
|
475
|
+
if (message.event === PROTOCOL_EVENTS.OUTPUT) {
|
|
476
|
+
const TerminalRenderer = window._TerminalRenderer;
|
|
477
|
+
if (TerminalRenderer && TerminalRenderer.handleOutput) {
|
|
478
|
+
TerminalRenderer.handleOutput(message);
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// zTable event emitted standalone (non-walker contexts, or fallback).
|
|
484
|
+
// In walker/chunk mode, zTable is now injected inline into render_chunk
|
|
485
|
+
// (see advanced_table.py + message_walker._resolve_zdata_reads).
|
|
486
|
+
if (message.event === PROTOCOL_EVENTS.ZTABLE) {
|
|
487
|
+
this.logger.log('[MessageHandler] Standalone zTable event received (non-chunk context)');
|
|
488
|
+
this.hooks.call('onDisplay', { ...message, display_event: PROTOCOL_EVENTS.ZTABLE });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Check for display events (supports multiple formats)
|
|
493
|
+
// - Old: {event: 'display', data: {...}}
|
|
494
|
+
// - New: {display_event: 'success', data: {...}}
|
|
495
|
+
// - Progress: {event: 'progress_bar', ...} → treated as display event
|
|
496
|
+
if (message.event === PROTOCOL_EVENTS.DISPLAY || message.type === PROTOCOL_EVENTS.DISPLAY || message.display_event) {
|
|
497
|
+
this.logger.debug('[MessageHandler] Display event:', message.display_event);
|
|
498
|
+
try {
|
|
499
|
+
this.hooks.call('onDisplay', message); // Pass full message with display_event
|
|
500
|
+
} catch (hookError) {
|
|
501
|
+
this.logger.error('[MessageHandler] Error in display event handler:', hookError);
|
|
502
|
+
this.hooks.call('onError', { type: 'display_error', error: hookError, message });
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Progress bar events - also route to display renderer
|
|
508
|
+
if (message.event === PROTOCOL_EVENTS.PROGRESS_BAR || message.event === PROTOCOL_EVENTS.PROGRESS_COMPLETE) {
|
|
509
|
+
message.display_event = PROTOCOL_EVENTS.PROGRESS_BAR;
|
|
510
|
+
this.hooks.call('onDisplay', message);
|
|
511
|
+
this.hooks.call('onProgressBar', message); // Also call specific hook for backwards compat
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (message.event === PROTOCOL_EVENTS.INPUT_REQUEST || message.type === PROTOCOL_EVENTS.INPUT_REQUEST) {
|
|
516
|
+
this.hooks.call('onInput', message);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (message.event === PROTOCOL_EVENTS.PROGRESS_UPDATE) {
|
|
521
|
+
this.hooks.call('onProgressUpdate', message);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (message.event === PROTOCOL_EVENTS.PROGRESS_COMPLETE) {
|
|
526
|
+
this.hooks.call('onProgressComplete', message);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Spinner events
|
|
531
|
+
if (message.event === PROTOCOL_EVENTS.SPINNER_START) {
|
|
532
|
+
this.hooks.call('onSpinnerStart', message);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (message.event === PROTOCOL_EVENTS.SPINNER_STOP) {
|
|
537
|
+
this.hooks.call('onSpinnerStop', message);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (message.event === PROTOCOL_EVENTS.SWIPER_INIT) {
|
|
542
|
+
this.hooks.call('onSwiperInit', message);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// App-level log event (zLogger / zos.app.log) — output to browser console
|
|
547
|
+
if (message.event === PROTOCOL_EVENTS.APP_LOG) {
|
|
548
|
+
const level = (message.level || 'INFO').toUpperCase();
|
|
549
|
+
const tag = message.tag ? `[${message.tag}] ` : '';
|
|
550
|
+
const line = `${tag}${message.message}`;
|
|
551
|
+
if (level === 'ERROR' || level === 'CRITICAL') console.error('[zLog]', line);
|
|
552
|
+
else if (level === 'WARNING') console.warn('[zLog]', line);
|
|
553
|
+
else if (level === 'DEBUG') console.debug('[zLog]', line);
|
|
554
|
+
else console.log('[zLog]', line);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// zFunc execution signal — backend console confirmation, not a UI event
|
|
559
|
+
if (message.event === PROTOCOL_EVENTS.ZFUNC_EXEC) {
|
|
560
|
+
if (message.stdout) console.log('[zFunc stdout]', message.stdout);
|
|
561
|
+
console.log('[zFunc]', message.spec, '→', message.result, message.success ? '✓' : '✗');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Otherwise, treat as broadcast
|
|
566
|
+
this._handleBroadcast(message);
|
|
567
|
+
|
|
568
|
+
} catch (error) {
|
|
569
|
+
this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] CRITICAL ERROR:', error);
|
|
570
|
+
this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] Error stack:', error.stack);
|
|
571
|
+
this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] Raw data:', data);
|
|
572
|
+
this.logger.log('[ERROR] Failed to parse message', { data, error });
|
|
573
|
+
this.hooks.call('onError', error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Send a message and wait for response
|
|
579
|
+
*/
|
|
580
|
+
async send(payload, sendFn, timeout = null) {
|
|
581
|
+
try {
|
|
582
|
+
// Validate message follows protocol
|
|
583
|
+
this._validateOutgoingMessage(payload);
|
|
584
|
+
|
|
585
|
+
// Attach session ID from HTTP cookie for session sync (WebSocket/HTTP bridge)
|
|
586
|
+
// Only attach for walker execution requests, not for form submissions
|
|
587
|
+
// (forms don't need session sync until AFTER successful login)
|
|
588
|
+
const sessionId = this._getSessionIdFromCookie();
|
|
589
|
+
if (sessionId && payload.event === PROTOCOL_EVENTS.EXECUTE_WALKER) {
|
|
590
|
+
payload._sessionId = sessionId;
|
|
591
|
+
this.logger.log('[MessageHandler] Attached session ID to walker execution');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const requestId = this.requestId++;
|
|
595
|
+
payload._requestId = requestId;
|
|
596
|
+
|
|
597
|
+
const timeoutMs = timeout || this.timeout;
|
|
598
|
+
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
const callback = { resolve, reject };
|
|
601
|
+
|
|
602
|
+
// Set timeout
|
|
603
|
+
callback.timeoutId = setTimeout(() => {
|
|
604
|
+
if (this.callbacks.has(requestId)) {
|
|
605
|
+
this.callbacks.delete(requestId);
|
|
606
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
607
|
+
}
|
|
608
|
+
}, timeoutMs);
|
|
609
|
+
|
|
610
|
+
this.callbacks.set(requestId, callback);
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
// Send message
|
|
614
|
+
const message = JSON.stringify(payload);
|
|
615
|
+
this.logger.log('Sending message', payload);
|
|
616
|
+
sendFn(message);
|
|
617
|
+
} catch (sendError) {
|
|
618
|
+
// Clean up callback on send failure
|
|
619
|
+
this.callbacks.delete(requestId);
|
|
620
|
+
if (callback.timeoutId) {
|
|
621
|
+
clearTimeout(callback.timeoutId);
|
|
622
|
+
}
|
|
623
|
+
reject(new Error(`Failed to send message: ${sendError.message}`));
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.logger.error('[MessageHandler] Error in send():', error);
|
|
628
|
+
throw error; // Propagate to caller
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Handle response to a request
|
|
634
|
+
* @private
|
|
635
|
+
*/
|
|
636
|
+
_handleResponse(requestId, message) {
|
|
637
|
+
try {
|
|
638
|
+
const callback = this.callbacks.get(requestId);
|
|
639
|
+
if (!callback) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
this.callbacks.delete(requestId);
|
|
644
|
+
|
|
645
|
+
// Clear timeout
|
|
646
|
+
if (callback.timeoutId) {
|
|
647
|
+
clearTimeout(callback.timeoutId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Resolve or reject
|
|
651
|
+
if (message.error) {
|
|
652
|
+
callback.reject(new Error(message.error));
|
|
653
|
+
} else {
|
|
654
|
+
// Return entire message (minus _requestId) for flexibility
|
|
655
|
+
// Some responses use 'result' field, others use 'success', 'data', etc.
|
|
656
|
+
const { _requestId, ...response } = message;
|
|
657
|
+
callback.resolve(response.result !== undefined ? response.result : response);
|
|
658
|
+
}
|
|
659
|
+
} catch (error) {
|
|
660
|
+
this.logger.error('[MessageHandler] Error handling response:', error);
|
|
661
|
+
// Attempt to reject the callback if it still exists
|
|
662
|
+
const callback = this.callbacks.get(requestId);
|
|
663
|
+
if (callback) {
|
|
664
|
+
this.callbacks.delete(requestId);
|
|
665
|
+
if (callback.timeoutId) {
|
|
666
|
+
clearTimeout(callback.timeoutId);
|
|
667
|
+
}
|
|
668
|
+
callback.reject(new Error(`Response handling error: ${error.message}`));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Handle broadcast message
|
|
675
|
+
* @private
|
|
676
|
+
*/
|
|
677
|
+
_handleBroadcast(message) {
|
|
678
|
+
try {
|
|
679
|
+
this.logger.debug('Broadcast received:', message.type);
|
|
680
|
+
this.hooks.call('onBroadcast', message);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
this.logger.error('[MessageHandler] Error handling broadcast:', error);
|
|
683
|
+
this.hooks.call('onError', { type: 'broadcast_error', error, message });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Send input response to server
|
|
689
|
+
*/
|
|
690
|
+
sendInputResponse(requestId, value, sendFn) {
|
|
691
|
+
const response = {
|
|
692
|
+
event: 'input_response',
|
|
693
|
+
requestId: requestId,
|
|
694
|
+
value: value
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
sendFn(JSON.stringify(response));
|
|
698
|
+
this.logger.log('Sent input response', response);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|