ai/neural/nn-orchestrator.js

/**
 * @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);