/**
* @fileoverview IframeBridge — Einheitliches Kommunikationssystem für iframe-basierte Module.
*
* Ersetzt das bisherige Mischsystem aus URL-Parametern, manuellen postMessage-Aufrufen
* und globalem Keyboard-Forwarding durch ein strukturiertes Host/Client-Protokoll.
*
* Architektur:
* - IframeBridgeHost (läuft in der Parent-Seite, steuert ein iframe)
* - IframeBridgeClient (läuft im iframe, kommuniziert mit Parent)
* - Beide nutzen ein gemeinsames Message-Schema mit Namespace:Action-Typen
*
* Lifecycle:
* 1. Client sendet SYSTEM:READY
* 2. Host empfängt → sendet CONFIG:INIT (+ flusht Queue)
* 3. Client empfängt → wendet Config an → sendet SYSTEM:READY_ACK
* 4. Host markiert ready=true, regulärer Betrieb beginnt
*
* Debug-Integration:
* Nutzt DebugConfig.log() mit den Domains CORE_IFRAME_BRIDGE, CORE_IFRAME_BRIDGE_HOST,
* CORE_IFRAME_BRIDGE_CLIENT. Aktivierung: DEBUG_CONFIG.DEBUG_CORE_IFRAME_BRIDGE = true
*
* @author Alexander Wolf
* @version 1.0
* @see docs/conventions/IFRAME_BRIDGE_PROTOCOL.md
*/
// ============================================================================
// SHARED PROTOCOL
// ============================================================================
/**
* Gemeinsames Protokoll für Message-Validierung und -Erzeugung.
* Wird intern von Host und Client genutzt.
*
* @namespace IframeBridgeProtocol
*/
const IframeBridgeProtocol = {
/** @type {string} Marker-Feld zur Erkennung von Bridge-Messages */
MARKER: '_bridge',
/** @type {string} Bridge-Protokollversion */
VERSION: '1.0',
/**
* Erzeugt ein standardisiertes Message-Objekt.
*
* @param {string} type - Hierarchischer Typ im Format NAMESPACE:ACTION
* @param {Object} [payload={}] - Beliebiges Datenobjekt
* @param {string} [sourceId=''] - Eindeutige Absender-Kennung
* @returns {{ _bridge: true, version: string, type: string, payload: Object, sourceId: string, timestamp: number, messageId: string }}
*/
createMessage(type, payload = {}, sourceId = '') {
return {
_bridge: true,
version: this.VERSION,
type,
payload,
sourceId,
timestamp: Date.now(),
messageId: this._generateId()
};
},
/**
* Prüft ob ein eingehendes Datenobjekt eine gültige Bridge-Message ist.
*
* @param {*} data - Das rohe event.data-Objekt
* @returns {boolean}
*/
isValidMessage(data) {
return data && data._bridge === true && typeof data.type === 'string';
},
/**
* Mapping von alten Message-Typen auf neue Namespace:Action-Typen.
* Ermöglicht Abwärtskompatibilität während der Migration.
*
* @type {Object<string, string>}
*/
LEGACY_TYPE_MAP: {
'keydown': 'INPUT:KEYBOARD',
'gameState': 'GAME:STATE',
'gameWon': 'GAME:WON',
'UPDATE_PARAMS': 'CONFIG:UPDATE',
'TREE_COMMAND': 'TREE:COMMAND',
'TREE_READY': 'SYSTEM:READY',
'NODE_CLICKED': 'TREE:NODE_CLICKED',
'NODE_FOCUSED': 'TREE:NODE_FOCUSED',
'NODE_EXPANSION_REQUEST': 'TREE:EXPANSION_REQUEST',
'VIZ_READY': 'SYSTEM:READY',
'VIZ_COMMAND': 'VIZ:COMMAND'
},
/**
* Konvertiert eine Legacy-Message in das neue Format.
* Gibt null zurück wenn keine Konvertierung möglich ist.
*
* @param {Object} data - Das rohe event.data-Objekt (ohne _bridge)
* @returns {Object|null} Konvertierte Message oder null
*/
convertLegacyMessage(data) {
if (!data || !data.type) return null;
const newType = this.LEGACY_TYPE_MAP[data.type];
if (!newType) return null;
// Payload extrahieren (alles außer 'type')
const payload = {};
for (const key of Object.keys(data)) {
if (key !== 'type') {
payload[key] = data[key];
}
}
// Spezielle Konvertierungen
if (data.type === 'keydown') {
payload.eventType = 'keydown';
}
if (data.type === 'TREE_COMMAND' && data.command) {
// TREE_COMMAND hat Payload in data.command
return this.createMessage(newType, data.command, 'legacy');
}
return this.createMessage(newType, payload, 'legacy');
},
/**
* Erzeugt eine kurze, eindeutige ID für Message-Korrelation.
*
* @private
* @returns {string} z.B. 'msg_a1b2c3d4'
*/
_generateId() {
return 'msg_' + Math.random().toString(36).substring(2, 10);
}
};
// ============================================================================
// IFRAME BRIDGE HOST (Parent-Seite)
// ============================================================================
/**
* Host-seitige Bridge zur Steuerung eines iframes.
* Läuft in der Parent-Seite und sendet Messages an das iframe.
*
* @class IframeBridgeHost
*
* @example
* const bridge = new IframeBridgeHost(document.getElementById('gameFrame'), {
* sourceId: 'learning-path-01',
* forwardKeyboard: ['ArrowLeft', 'ArrowRight'],
* initConfig: {
* ui: { hideAi: true, hideBackBtn: true },
* runtime: { level: 2 }
* }
* });
*
* bridge.on('GAME:WON', (payload) => {
* console.log('Gewonnen mit', payload.moves, 'Zügen');
* });
*/
class IframeBridgeHost {
/**
* @param {HTMLIFrameElement} iframeElement - Das zu steuernde iframe-Element
* @param {Object} [options={}] - Konfigurationsoptionen
* @param {string} [options.sourceId='host'] - Eindeutige Kennung dieses Hosts
* @param {boolean|string[]} [options.forwardKeyboard=false] - Keyboard-Forwarding (true=alle, Array=Filter)
* @param {Object} [options.initConfig=null] - Config, die nach SYSTEM:READY gesendet wird
* @param {Object} [options.initConfig.ui] - UI-Konfiguration (hideControls, etc.)
* @param {Object} [options.initConfig.runtime] - Runtime-Konfiguration (level, maxDepth, etc.)
* @param {number} [options.handshakeTimeout=5000] - Timeout für den Handshake in ms
* @param {string} [options.targetOrigin=BRIDGE_TARGET_ORIGIN] - postMessage targetOrigin
* @param {boolean} [options.acceptLegacy=true] - Legacy-Messages akzeptieren und konvertieren
*/
constructor(iframeElement, options = {}) {
if (!iframeElement || iframeElement.tagName !== 'IFRAME') {
this._log('Constructor called without valid iframe element', { element: iframeElement }, 'error');
throw new Error('IframeBridgeHost: Valid HTMLIFrameElement required');
}
/** @type {HTMLIFrameElement} */
this.iframe = iframeElement;
/** @type {string} */
this.sourceId = options.sourceId || 'host';
/** @type {string} Für lokale Entwicklung: '*'. Production: window.location.origin. */
this.targetOrigin = options.targetOrigin || (typeof BRIDGE_TARGET_ORIGIN !== 'undefined' ? BRIDGE_TARGET_ORIGIN : '*');
/** @type {boolean} */
this.acceptLegacy = options.acceptLegacy !== undefined ? options.acceptLegacy : true;
/** @type {boolean} Ist die Bridge betriebsbereit? */
this.ready = false;
/** @type {Object|null} Initiale Konfiguration */
this._initConfig = options.initConfig || null;
/** @type {number} Handshake-Timeout in ms */
this._handshakeTimeout = options.handshakeTimeout || 5000;
/** @type {Array<Object>} Gepufferte Messages (vor Handshake) */
this._queue = [];
/** @type {Map<string, Set<Function>>} Event-Listener */
this._listeners = new Map();
/** @type {Function|null} Gebundener Message-Listener für Cleanup */
this._boundMessageHandler = null;
/** @type {Function|null} Gebundener Keyboard-Listener für Cleanup */
this._boundKeyboardHandler = null;
/** @type {number|null} Handshake-Timeout-Timer */
this._handshakeTimer = null;
this._log('Initializing', { sourceId: this.sourceId, hasConfig: !!this._initConfig });
// Message-Listener registrieren
this._boundMessageHandler = (event) => this._handleMessage(event);
window.addEventListener('message', this._boundMessageHandler);
// Keyboard-Forwarding einrichten
if (options.forwardKeyboard) {
this._setupKeyboardForwarding(options.forwardKeyboard);
}
// Handshake-Timeout starten
this._handshakeTimer = setTimeout(() => {
if (!this.ready) {
this._log('Handshake timeout — forcing ready state', {
timeout: this._handshakeTimeout,
queueSize: this._queue.length
}, 'warn');
this._setReady();
}
}, this._handshakeTimeout);
}
// ==================== PUBLIC API ====================
/**
* Sendet eine Message an das iframe.
* Wird gepuffert falls die Bridge noch nicht bereit ist.
*
* @param {string} type - Hierarchischer Typ (z.B. 'CONFIG:UPDATE')
* @param {Object} [payload={}] - Datenobjekt
*/
send(type, payload = {}) {
const message = IframeBridgeProtocol.createMessage(type, payload, this.sourceId);
if (!this.ready) {
this._log('Queueing message (not ready)', { type, queueSize: this._queue.length + 1 });
this._queue.push(message);
return;
}
this._postMessage(message);
}
/**
* Registriert einen Event-Listener für einen Message-Typ.
* Unterstützt Wildcard-Matching via Namespace-Prefix (z.B. 'GAME:' für alle GAME-Events).
*
* @param {string} type - Message-Typ oder Namespace-Prefix
* @param {Function} callback - Callback(payload, fullMessage)
* @returns {IframeBridgeHost} this (für Chaining)
*/
on(type, callback) {
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type).add(callback);
return this;
}
/**
* Registriert einen einmaligen Event-Listener.
*
* @param {string} type - Message-Typ
* @param {Function} callback - Callback(payload, fullMessage)
* @returns {IframeBridgeHost} this (für Chaining)
*/
once(type, callback) {
const wrapper = (payload, message) => {
this.off(type, wrapper);
callback(payload, message);
};
wrapper._original = callback;
return this.on(type, wrapper);
}
/**
* Entfernt einen Event-Listener.
*
* @param {string} type - Message-Typ
* @param {Function} [callback] - Spezifischer Callback (ohne: alle für diesen Typ)
* @returns {IframeBridgeHost} this (für Chaining)
*/
off(type, callback) {
if (!callback) {
this._listeners.delete(type);
} else if (this._listeners.has(type)) {
const set = this._listeners.get(type);
set.delete(callback);
// Auch wrapped once-Callbacks finden
for (const fn of set) {
if (fn._original === callback) {
set.delete(fn);
break;
}
}
}
return this;
}
/**
* Gibt ein Promise zurück, das resolved wenn die Bridge bereit ist.
*
* @returns {Promise<void>}
*/
waitForReady() {
if (this.ready) return Promise.resolve();
return new Promise((resolve) => {
this.once('SYSTEM:READY_ACK', () => resolve());
// Auch bei Timeout resolven
const timer = setTimeout(() => resolve(), this._handshakeTimeout + 100);
this.once('SYSTEM:READY_ACK', () => clearTimeout(timer));
});
}
/**
* Räumt alle Listener und Timer auf.
* Muss aufgerufen werden wenn der Host nicht mehr benötigt wird.
*/
destroy() {
this._log('Destroying bridge');
if (this._boundMessageHandler) {
window.removeEventListener('message', this._boundMessageHandler);
this._boundMessageHandler = null;
}
if (this._boundKeyboardHandler) {
document.removeEventListener('keydown', this._boundKeyboardHandler);
this._boundKeyboardHandler = null;
}
if (this._handshakeTimer) {
clearTimeout(this._handshakeTimer);
this._handshakeTimer = null;
}
this._listeners.clear();
this._queue = [];
this.ready = false;
}
// ==================== INTERNAL ====================
/**
* Verarbeitet eingehende postMessage-Events.
*
* @private
* @param {MessageEvent} event
*/
_handleMessage(event) {
let message = null;
if (IframeBridgeProtocol.isValidMessage(event.data)) {
message = event.data;
} else if (this.acceptLegacy) {
message = IframeBridgeProtocol.convertLegacyMessage(event.data);
}
if (!message) return;
this._log('Received', { type: message.type, sourceId: message.sourceId });
// Handshake: SYSTEM:READY vom Client
if (message.type === 'SYSTEM:READY' && !this.ready) {
this._log('Client READY signal received — sending CONFIG:INIT');
if (this._initConfig) {
this._postMessage(
IframeBridgeProtocol.createMessage('CONFIG:INIT', this._initConfig, this.sourceId)
);
}
// Queue sofort flushen
this._setReady();
}
// Handshake: SYSTEM:READY_ACK vom Client
if (message.type === 'SYSTEM:READY_ACK') {
this._log('Client READY_ACK received — handshake complete');
}
// Event an Listener dispatchen
this._dispatch(message);
}
/**
* Dispatcht eine Message an registrierte Listener.
* Prüft exakte Matches und Namespace-Prefix-Matches.
*
* @private
* @param {Object} message
*/
_dispatch(message) {
const { type, payload } = message;
// Exakter Match
if (this._listeners.has(type)) {
for (const cb of this._listeners.get(type)) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error', { type, error: err.message }, 'error');
}
}
}
// Namespace-Prefix-Match (z.B. 'GAME:' matcht 'GAME:WON')
const namespace = type.split(':')[0] + ':';
if (namespace !== type && this._listeners.has(namespace)) {
for (const cb of this._listeners.get(namespace)) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error (namespace)', { type, error: err.message }, 'error');
}
}
}
// Wildcard
if (this._listeners.has('*')) {
for (const cb of this._listeners.get('*')) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error (wildcard)', { type, error: err.message }, 'error');
}
}
}
}
/**
* Sendet eine Message direkt via postMessage an das iframe.
*
* @private
* @param {Object} message - Fertige Bridge-Message
*/
_postMessage(message) {
if (!this.iframe || !this.iframe.contentWindow) {
this._log('Cannot post — iframe contentWindow not available', { type: message.type }, 'error');
return;
}
this.iframe.contentWindow.postMessage(message, this.targetOrigin);
this._log('Sent', { type: message.type, payloadKeys: Object.keys(message.payload) });
}
/**
* Markiert die Bridge als bereit und flusht die Queue.
*
* @private
*/
_setReady() {
if (this.ready) return;
this.ready = true;
if (this._handshakeTimer) {
clearTimeout(this._handshakeTimer);
this._handshakeTimer = null;
}
// Queue flushen
if (this._queue.length > 0) {
this._log('Flushing queue', { count: this._queue.length });
for (const msg of this._queue) {
this._postMessage(msg);
}
this._queue = [];
}
}
/**
* Richtet automatisches Keyboard-Forwarding ein.
*
* @private
* @param {boolean|string[]} config - true für alle Keys, Array für Filter
*/
_setupKeyboardForwarding(config) {
const keyFilter = Array.isArray(config) ? config : null;
this._boundKeyboardHandler = (e) => {
if (keyFilter && !keyFilter.includes(e.key)) return;
this.send('INPUT:KEYBOARD', {
key: e.key,
code: e.code,
shift: e.shiftKey,
ctrl: e.ctrlKey,
alt: e.altKey,
eventType: 'keydown'
});
};
document.addEventListener('keydown', this._boundKeyboardHandler);
this._log('Keyboard forwarding enabled', { filter: keyFilter || 'all keys' });
}
/**
* Internes Logging via DebugConfig.
*
* @private
* @param {string} message - Log-Nachricht
* @param {Object} [payload] - Strukturiertes Datenobjekt
* @param {string} [level='debug'] - Log-Level
*/
_log(message, payload, level = 'debug') {
if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
const domain = DEBUG_DOMAINS.CORE_IFRAME_BRIDGE_HOST || DEBUG_DOMAINS.CORE_IFRAME_BRIDGE || 'CORE_IFRAME_BRIDGE_HOST';
if (payload) {
DebugConfig.log(domain, level, `[IframeBridgeHost:${this.sourceId}] ${message}`, payload);
} else {
DebugConfig.log(domain, level, `[IframeBridgeHost:${this.sourceId}] ${message}`);
}
}
}
}
// ============================================================================
// IFRAME BRIDGE CLIENT (iframe-Seite)
// ============================================================================
/**
* Client-seitige Bridge, die im iframe läuft.
* Kommuniziert mit dem Parent via window.parent.postMessage.
*
* @class IframeBridgeClient
*
* @example
* const bridge = new IframeBridgeClient({ sourceId: 'rotatebox-game' });
*
* bridge.on('CONFIG:INIT', (config) => {
* if (config.ui?.hideControls) hideToolbar();
* if (config.runtime?.level) loadLevel(config.runtime.level);
* });
*
* bridge.on('INPUT:KEYBOARD', (payload) => {
* if (payload.key === 'ArrowLeft') rotateLeft();
* });
*
* // Spielstatus nach oben melden
* bridge.send('GAME:WON', { moves: 6 });
*/
class IframeBridgeClient {
/**
* @param {Object} [options={}] - Konfigurationsoptionen
* @param {string} [options.sourceId='client'] - Eindeutige Kennung dieses Clients
* @param {string} [options.clientType='generic'] - Typ des Clients ('game', 'playground', 'viz')
* @param {string} [options.targetOrigin=BRIDGE_TARGET_ORIGIN] - postMessage targetOrigin
* @param {boolean} [options.acceptLegacy=true] - Legacy-Messages akzeptieren und konvertieren
* @param {boolean} [options.autoReady=true] - Automatisch SYSTEM:READY senden
* @param {string[]} [options.autoRelayToParent=[]] - Namespace-Prefixe, die automatisch an window.parent weitergeleitet werden (z.B. ['TREE:', 'NODE:'])
*/
constructor(options = {}) {
/** @type {string} */
this.sourceId = options.sourceId || 'client';
/** @type {string} */
this.clientType = options.clientType || 'generic';
/** @type {string} Für lokale Entwicklung: '*'. Production: window.location.origin. */
this.targetOrigin = options.targetOrigin || (typeof BRIDGE_TARGET_ORIGIN !== 'undefined' ? BRIDGE_TARGET_ORIGIN : '*');
/** @type {boolean} */
this.acceptLegacy = options.acceptLegacy !== undefined ? options.acceptLegacy : true;
/** @type {boolean} Ist die Bridge betriebsbereit? */
this.ready = false;
/** @type {Object} Empfangene CONFIG:INIT-Daten */
this.config = {};
/** @type {string[]} Namespace-Prefixe für Auto-Relay an window.parent (3-Ebenen-Modell) */
this._autoRelayPrefixes = options.autoRelayToParent || [];
/** @type {Map<string, Set<Function>>} Event-Listener */
this._listeners = new Map();
/** @type {Function|null} Gebundener Message-Listener */
this._boundMessageHandler = null;
/** @type {boolean} Läuft in einem iframe? */
this._isInIframe = (window.parent !== window);
this._log('Initializing', {
sourceId: this.sourceId,
clientType: this.clientType,
isInIframe: this._isInIframe,
autoRelayPrefixes: this._autoRelayPrefixes
});
// Message-Listener registrieren
this._boundMessageHandler = (event) => this._handleMessage(event);
window.addEventListener('message', this._boundMessageHandler);
// Automatisch SYSTEM:READY senden
if (options.autoReady !== false) {
// Kurz warten bis DOM stabil ist
if (document.readyState === 'complete') {
this._sendReady();
} else {
window.addEventListener('load', () => this._sendReady());
}
}
}
// ==================== PUBLIC API ====================
/**
* Sendet eine Message an den Parent.
* Im Standalone-Betrieb (kein iframe) wird die Message ignoriert.
*
* @param {string} type - Hierarchischer Typ (z.B. 'GAME:WON')
* @param {Object} [payload={}] - Datenobjekt
*/
send(type, payload = {}) {
if (!this._isInIframe) {
this._log('Not in iframe — message discarded', { type });
return;
}
const message = IframeBridgeProtocol.createMessage(type, payload, this.sourceId);
window.parent.postMessage(message, this.targetOrigin);
this._log('Sent to parent', { type, payloadKeys: Object.keys(payload) });
}
/**
* Registriert einen Event-Listener für einen Message-Typ.
*
* @param {string} type - Message-Typ oder Namespace-Prefix
* @param {Function} callback - Callback(payload, fullMessage)
* @returns {IframeBridgeClient} this (für Chaining)
*/
on(type, callback) {
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type).add(callback);
return this;
}
/**
* Registriert einen einmaligen Event-Listener.
*
* @param {string} type - Message-Typ
* @param {Function} callback - Callback(payload, fullMessage)
* @returns {IframeBridgeClient} this (für Chaining)
*/
once(type, callback) {
const wrapper = (payload, message) => {
this.off(type, wrapper);
callback(payload, message);
};
wrapper._original = callback;
return this.on(type, wrapper);
}
/**
* Entfernt einen Event-Listener.
*
* @param {string} type - Message-Typ
* @param {Function} [callback] - Spezifischer Callback
* @returns {IframeBridgeClient} this (für Chaining)
*/
off(type, callback) {
if (!callback) {
this._listeners.delete(type);
} else if (this._listeners.has(type)) {
const set = this._listeners.get(type);
set.delete(callback);
for (const fn of set) {
if (fn._original === callback) {
set.delete(fn);
break;
}
}
}
return this;
}
/**
* Sendet manuell das SYSTEM:READY-Signal.
* Normalerweise automatisch (autoReady=true).
*/
sendReady() {
this._sendReady();
}
/**
* Räumt alle Listener auf.
*/
destroy() {
this._log('Destroying bridge');
if (this._boundMessageHandler) {
window.removeEventListener('message', this._boundMessageHandler);
this._boundMessageHandler = null;
}
this._listeners.clear();
this.ready = false;
}
// ==================== INTERNAL ====================
/**
* Sendet das SYSTEM:READY-Signal an den Parent.
*
* @private
*/
_sendReady() {
this._log('Sending SYSTEM:READY');
this.send('SYSTEM:READY', { clientType: this.clientType });
}
/**
* Verarbeitet eingehende postMessage-Events.
*
* @private
* @param {MessageEvent} event
*/
_handleMessage(event) {
let message = null;
if (IframeBridgeProtocol.isValidMessage(event.data)) {
message = event.data;
} else if (this.acceptLegacy) {
message = IframeBridgeProtocol.convertLegacyMessage(event.data);
}
if (!message) return;
this._log('Received', { type: message.type, sourceId: message.sourceId });
// CONFIG:INIT verarbeiten
if (message.type === 'CONFIG:INIT') {
this.config = message.payload || {};
this.ready = true;
this._log('Config applied', { config: this.config });
// READY_ACK senden
this.send('SYSTEM:READY_ACK', {});
}
// Auto-Relay: Bestimmte Namespaces automatisch an window.parent weiterleiten
// Ermöglicht 3-Ebenen-Kommunikation (TreeViz → Playground → Learning Path)
if (this._autoRelayPrefixes.length > 0 && this._isInIframe) {
const shouldRelay = this._autoRelayPrefixes.some(prefix => message.type.startsWith(prefix));
if (shouldRelay) {
const relayPayload = {
...message.payload,
relayedFrom: message.sourceId || 'unknown'
};
const relayMessage = IframeBridgeProtocol.createMessage(
message.type, relayPayload, this.sourceId
);
window.parent.postMessage(relayMessage, this.targetOrigin);
this._log('Auto-relayed to parent', {
type: message.type,
relayedFrom: message.sourceId,
via: this.sourceId
});
}
}
// An Listener dispatchen
this._dispatch(message);
}
/**
* Dispatcht eine Message an registrierte Listener.
*
* @private
* @param {Object} message
*/
_dispatch(message) {
const { type, payload } = message;
// Exakter Match
if (this._listeners.has(type)) {
for (const cb of this._listeners.get(type)) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error', { type, error: err.message }, 'error');
}
}
}
// Namespace-Prefix-Match
const namespace = type.split(':')[0] + ':';
if (namespace !== type && this._listeners.has(namespace)) {
for (const cb of this._listeners.get(namespace)) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error (namespace)', { type, error: err.message }, 'error');
}
}
}
// Wildcard
if (this._listeners.has('*')) {
for (const cb of this._listeners.get('*')) {
try {
cb(payload, message);
} catch (err) {
this._log('Listener error (wildcard)', { type, error: err.message }, 'error');
}
}
}
}
/**
* Internes Logging via DebugConfig.
*
* @private
* @param {string} message - Log-Nachricht
* @param {Object} [payload] - Strukturiertes Datenobjekt
* @param {string} [level='debug'] - Log-Level
*/
_log(message, payload, level = 'debug') {
if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
const domain = DEBUG_DOMAINS.CORE_IFRAME_BRIDGE_CLIENT || DEBUG_DOMAINS.CORE_IFRAME_BRIDGE || 'CORE_IFRAME_BRIDGE_CLIENT';
if (payload) {
DebugConfig.log(domain, level, `[IframeBridgeClient:${this.sourceId}] ${message}`, payload);
} else {
DebugConfig.log(domain, level, `[IframeBridgeClient:${this.sourceId}] ${message}`);
}
}
}
}
// ============================================================================
// GLOBAL EXPORTS
// ============================================================================
if (typeof window !== 'undefined') {
window.IframeBridgeProtocol = IframeBridgeProtocol;
window.IframeBridgeHost = IframeBridgeHost;
window.IframeBridgeClient = IframeBridgeClient;
}