/**
* @fileoverview NNOrchestrator — Main-Thread Orchestrator für das Neuronale Netz.
*
* Verantwortlich für:
* - Erstellung und Verwaltung des Web Workers
* - Throttling der Worker-Snapshots auf UI-Framerate (60fps)
* - Weiterleitung der Snapshots an die IframeBridge (NN:SNAPSHOT etc.)
* - Empfang von Nutzer-Interaktionen (Gewichts-Änderungen, Hyperparameter)
* - Entkopplung von High-Speed-Training und Low-Speed-Visualisierung
*
* Architektur:
* Worker ──(postMessage)──▶ NNOrchestrator ──(IframeBridge)──▶ View (iframe)
* View ──(IframeBridge)──▶ NNOrchestrator ──(postMessage)──▶ Worker
*
* Convention: Keine direkten DOM-Aufrufe! Keine console.log!
*
* @author Alexander Wolf
* @see docs/architecture/NEURAL_NET_ARCHITECTURE.md
* @see docs/conventions/IFRAME_BRIDGE_PROTOCOL.md
*/
/* global IframeBridgeHost, IframeBridgeClient, DebugConfig, DEBUG_DOMAINS */
/**
* @typedef {Object} OrchestratorConfig
* @property {IframeBridgeHost|IframeBridgeClient} bridge - Die IframeBridge-Instanz
* @property {string} [workerPath='js/ai/neural/worker/nn-training-worker.js'] - Pfad zum Worker
* @property {function(NetworkSnapshot): void} [onSnapshot] - Optional: Lokaler Snapshot-Callback
* @property {function(TrainingResult): void} [onTrainingComplete] - Optional: Training-Ende Callback
* @property {function(Object): void} [onError] - Optional: Fehler-Callback
*/
class NNOrchestrator {
/**
* Erstellt den Orchestrator und startet den Web Worker.
*
* @param {OrchestratorConfig} config
*/
constructor(config) {
const {
bridge,
workerPath = 'js/ai/neural/worker/nn-training-worker.js',
onSnapshot = null,
onTrainingComplete = null,
onError = null
} = config;
/**
* Bridge-Instanz für die Kommunikation mit der View.
* @type {IframeBridgeHost|IframeBridgeClient}
*/
this.bridge = bridge;
/**
* Lokaler Snapshot-Callback (zusätzlich zur Bridge).
* @type {((arg0: NetworkSnapshot) => void)|null}
*/
this.onSnapshot = onSnapshot;
/**
* Lokaler Training-Complete-Callback.
* @type {((arg0: TrainingResult) => void)|null}
*/
this.onTrainingComplete = onTrainingComplete;
/**
* Lokaler Error-Callback.
* @type {((arg0: Object) => void)|null}
*/
this.onError = onError;
/**
* Der Web Worker für das Training.
* @type {Worker|null}
*/
this.worker = null;
/**
* Letzter empfangener Snapshot (Latest-Wins Buffer).
* @type {any|null}
*/
this._latestSnapshot = null;
/**
* Flag: Ist ein requestAnimationFrame geplant?
* @type {boolean}
*/
this._rafScheduled = false;
/**
* Flag: Läuft gerade ein Training?
* @type {boolean}
*/
this.isTraining = false;
/**
* Aktuelle Netzwerk-Topologie (für Referenz).
* @type {NetworkTopology|null}
*/
this.currentTopology = null;
// Worker und Bridge initialisieren
this._initWorker(workerPath);
this._initBridgeListeners();
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'NNOrchestrator initialized', { workerPath });
}
// ========================================================================
// PUBLIC API
// ========================================================================
/**
* Erstellt ein neues Netzwerk im Worker.
*
* @param {NetworkTopology} topology - Netzwerk-Topologie
*/
createNetwork(topology) {
this.currentTopology = topology;
this._sendToWorker('CREATE_NETWORK', topology);
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'Network creation requested:', { topology });
}
/**
* Startet das Training im Worker.
*
* @param {TrainingSample[]} dataset - Trainingsdaten
* @param {number} epochs - Anzahl der Epochen
*/
startTraining(dataset, epochs) {
this.isTraining = true;
this._sendToWorker('START_TRAINING', { dataset, epochs });
// Bridge-Event für die View
this.bridge.send('NN:TRAINING_START', {
topology: this.currentTopology,
epochs
});
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'Training started:', { datasetSize: dataset.length, epochs });
}
/**
* Stoppt das laufende Training.
*/
stopTraining() {
this._sendToWorker('STOP_TRAINING', {});
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug', 'Training stop requested');
}
/**
* Fordert eine einzelne Vorhersage an.
*
* @param {number[]} input - Eingabedaten
* @param {number[]} [legalMask] - Maske für legale Züge (Game-KI)
*/
predict(input, legalMask = undefined) {
this._sendToWorker('FORWARD_PASS', { input, legalMask });
}
/**
* Setzt ein Gewicht manuell (Nutzer-Interaktion).
*
* @param {number} layerIndex
* @param {number} neuronIndex
* @param {number} weightIndex
* @param {number} value
*/
setWeight(layerIndex, neuronIndex, weightIndex, value) {
this._sendToWorker('SET_WEIGHT', { layerIndex, neuronIndex, weightIndex, value });
}
/**
* Setzt einen Bias manuell.
*
* @param {number} layerIndex
* @param {number} neuronIndex
* @param {number} value
*/
setBias(layerIndex, neuronIndex, value) {
this._sendToWorker('SET_BIAS', { layerIndex, neuronIndex, value });
}
/**
* Ändert die Lernrate.
*
* @param {number} rate
*/
setLearningRate(rate) {
this._sendToWorker('SET_LEARNING_RATE', { learningRate: rate });
}
/**
* Fordert einen vollständigen Snapshot an.
*/
requestSnapshot() {
this._sendToWorker('GET_SNAPSHOT', {});
}
/**
* Lädt gespeicherte Gewichte.
*
* @param {Object} weights - Serialisierte Gewichte (von NeuralNetwork.serialize())
*/
loadWeights(weights) {
this._sendToWorker('LOAD_WEIGHTS', weights);
}
/**
* Beendet den Worker und räumt auf.
*/
destroy() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this._removeBridgeListeners();
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug', 'NNOrchestrator destroyed');
}
// ========================================================================
// WORKER MANAGEMENT (Private)
// ========================================================================
/**
* Erstellt und konfiguriert den Web Worker.
*
* @param {string} workerPath
* @private
*/
_initWorker(workerPath) {
try {
this.worker = new Worker(workerPath);
this.worker.onmessage = this._handleWorkerMessage.bind(this);
this.worker.onerror = this._handleWorkerError.bind(this);
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'error',
'Worker creation failed:', { error: errMsg, workerPath });
if (this.onError) this.onError({ message: 'Worker creation failed', error });
}
}
/**
* Verarbeitet Messages vom Worker.
* Implementiert die "Latest-Wins" Throttle-Strategie:
* Snapshots werden gepuffert und nur per requestAnimationFrame weitergeleitet.
*
* @param {MessageEvent} event
* @private
*/
_handleWorkerMessage(event) {
const { type, payload } = event.data;
switch (type) {
case 'NETWORK_CREATED':
this.bridge.send('NN:SNAPSHOT', payload);
if (this.onSnapshot) this.onSnapshot(payload);
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'Network created in worker:', { topology: payload.topology });
break;
case 'SNAPSHOT':
// Latest-Wins: Überschreibe immer den neuesten Snapshot
this._latestSnapshot = payload;
this._scheduleSnapshotDispatch();
break;
case 'TRAINING_STARTED':
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'Worker training started:', payload);
break;
case 'TRAINING_COMPLETE':
this.isTraining = false;
// Finalen Snapshot sofort senden (nicht throtteln)
this.bridge.send('NN:TRAINING_COMPLETE', {
totalEpochs: payload.totalEpochs,
finalLoss: payload.finalLoss
});
if (payload.snapshot) {
this.bridge.send('NN:SNAPSHOT', payload.snapshot);
}
if (this.onTrainingComplete) this.onTrainingComplete(payload);
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'Training complete:', {
epochs: payload.totalEpochs,
loss: payload.finalLoss
});
break;
case 'PREDICTION':
this.bridge.send('NN:PREDICTION', payload);
break;
case 'WEIGHTS_LOADED':
this.requestSnapshot(); // Neuen Snapshot nach Load anfordern
break;
case 'ERROR':
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'error',
'Worker error:', payload);
this.bridge.send('NN:ERROR', payload);
if (this.onError) this.onError(payload);
break;
}
}
/**
* plant das Senden des neuesten Snapshots per requestAnimationFrame.
* Garantiert: Max 1 Snapshot pro Frame (~60fps), immer der neueste.
*
* @private
*/
_scheduleSnapshotDispatch() {
if (this._rafScheduled) return; // Bereits geplant
this._rafScheduled = true;
requestAnimationFrame(() => {
this._rafScheduled = false;
if (this._latestSnapshot) {
// An Bridge senden (→ View)
this.bridge.send('NN:SNAPSHOT', this._latestSnapshot);
// Epochen-Statistik separat senden
this.bridge.send('NN:TRAINING_STEP', {
epoch: this._latestSnapshot.epoch,
loss: this._latestSnapshot.loss,
accuracy: this._latestSnapshot.accuracy,
progress: this._latestSnapshot.progress
});
// Lokaler Callback
if (this.onSnapshot) this.onSnapshot(this._latestSnapshot);
this._latestSnapshot = null;
}
});
}
/**
* Worker-Fehler Handler.
*
* @param {ErrorEvent} error
* @private
*/
_handleWorkerError(error) {
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'critical',
'Worker runtime error:', {
message: error.message,
filename: error.filename,
lineno: error.lineno
});
if (this.onError) {
this.onError({
message: 'Worker runtime error',
details: error.message
});
}
}
/**
* Sendet eine Message an den Worker.
*
* @param {string} type - Befehlstyp
* @param {Object} payload - Daten
* @private
*/
_sendToWorker(type, payload) {
if (!this.worker) {
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'error',
'Cannot send to worker — worker not initialized:', { type });
return;
}
this.worker.postMessage({ type, payload });
}
// ========================================================================
// BRIDGE LISTENERS (Private)
// ========================================================================
/**
* Registriert Bridge-Listener für eingehende Befehle von der View.
* @private
*/
_initBridgeListeners() {
/**
* Gebundene Referenzen für späteres off().
* @type {Object<string, function>}
*/
this._bridgeHandlers = {
'NN:TOPOLOGY_CHANGE': (payload) => {
this.createNetwork(payload);
},
'NN:WEIGHT_CHANGE': (payload) => {
this.setWeight(payload.layer, payload.neuron, payload.weightIdx, payload.value);
},
'NN:HYPERPARAMS': (payload) => {
if (payload.learningRate !== undefined) {
this.setLearningRate(payload.learningRate);
}
},
'NN:PREDICT': (payload) => {
this.predict(payload.input, payload.legalMask);
},
'NN:START_TRAINING': (payload) => {
this.startTraining(payload.dataset, payload.epochs);
},
'NN:STOP_TRAINING': () => {
this.stopTraining();
}
};
for (const [type, handler] of Object.entries(this._bridgeHandlers)) {
this.bridge.on(type, handler);
}
}
/**
* Entfernt Bridge-Listener.
* @private
*/
_removeBridgeListeners() {
if (!this._bridgeHandlers) return;
for (const [type, handler] of Object.entries(this._bridgeHandlers)) {
this.bridge.off(type, handler);
}
this._bridgeHandlers = null;
}
}
// Export für globalen Zugriff
(function(root) {
root.NNOrchestrator = NNOrchestrator;
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this);