"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamProjector = exports.SSE_RING_BUFFER_SIZE = void 0;
const dataContract_1 = require("../../types/dataContract");
/** Max events retained in the per-session ring buffer. */
exports.SSE_RING_BUFFER_SIZE = 200;
class StreamProjector {
    withObservability(payload, observability) {
        if (!observability)
            return payload;
        const next = { ...payload };
        if (typeof observability.runId === 'string' && observability.runId.trim()) {
            next.runId = observability.runId.trim();
        }
        if (typeof observability.requestId === 'string' && observability.requestId.trim()) {
            next.requestId = observability.requestId.trim();
        }
        if (typeof observability.runSequence === 'number' && Number.isFinite(observability.runSequence)) {
            next.runSequence = Math.max(0, Math.floor(observability.runSequence));
        }
        return next;
    }
    setSseHeaders(res) {
        res.setHeader('Content-Type', 'text/event-stream');
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        res.setHeader('X-Accel-Buffering', 'no');
    }
    sendEvent(res, eventType, payload, seqId) {
        if (seqId !== undefined) {
            res.write(`id: ${seqId}\n`);
        }
        res.write(`event: ${eventType}\n`);
        res.write(`data: ${JSON.stringify(payload)}\n\n`);
    }
    sendConnected(res, payload) {
        this.sendEvent(res, 'connected', payload);
    }
    sendError(res, errorMessage, observability) {
        this.sendEvent(res, 'error', this.withObservability({ error: errorMessage, message: errorMessage, timestamp: Date.now() }, observability));
    }
    sendEnd(res, observability) {
        this.sendEvent(res, 'end', this.withObservability({ timestamp: Date.now() }, observability));
    }
    bindKeepAlive(req, res, intervalMs = 30000) {
        const keepAlive = setInterval(() => {
            try {
                res.write(`: keep-alive\n\n`);
            }
            catch {
                clearInterval(keepAlive);
            }
        }, intervalMs);
        req.on('close', () => {
            clearInterval(keepAlive);
        });
    }
    broadcastStreamingUpdate(sessionId, clients, update, options = {}) {
        const eventType = update.type;
        let eventData;
        if ((0, dataContract_1.isDataEvent)(eventType)) {
            const envelopes = Array.isArray(update.content) ? update.content : [update.content];
            for (let i = 0; i < envelopes.length; i++) {
                const envelope = envelopes[i];
                const validationErrors = (0, dataContract_1.validateDataEnvelope)(envelope);
                if (validationErrors.length > 0) {
                    options.onDataEnvelopeValidationWarning?.({
                        sessionId,
                        envelopeIndex: i,
                        errors: validationErrors,
                        envelope: {
                            metaType: envelope?.meta?.type,
                            metaSource: envelope?.meta?.source,
                            displayLayer: envelope?.display?.layer,
                            displayFormat: envelope?.display?.format,
                        },
                    });
                }
            }
            const validEnvelopes = envelopes.filter((envelope) => !!envelope && !!envelope.data);
            if (validEnvelopes.length > 0) {
                options.onValidDataEnvelopes?.(validEnvelopes);
            }
            eventData = JSON.stringify(this.withObservability({
                type: 'data',
                id: update.id || (0, dataContract_1.generateEventId)('sse', sessionId),
                envelope: update.content,
                timestamp: update.timestamp,
            }, options.observability));
        }
        else if ((0, dataContract_1.isLegacySkillEvent)(eventType)) {
            eventData = JSON.stringify(this.withObservability({
                type: update.type,
                id: update.id || (0, dataContract_1.generateEventId)('sse', sessionId),
                data: update.content,
                timestamp: update.timestamp,
            }, options.observability));
        }
        else {
            eventData = JSON.stringify(this.withObservability({
                type: update.type,
                id: update.id || (0, dataContract_1.generateEventId)('sse', sessionId),
                data: update.content,
                timestamp: update.timestamp,
            }, options.observability));
        }
        // Buffer the event for replay on reconnect
        if (options.seqId !== undefined) {
            options.onBufferedEvent?.({ seqId: options.seqId, eventType, eventData });
        }
        for (const client of clients) {
            try {
                if (options.seqId !== undefined) {
                    client.write(`id: ${options.seqId}\n`);
                }
                client.write(`event: ${eventType}\n`);
                client.write(`data: ${eventData}\n\n`);
            }
            catch {
                // Ignore broken pipe errors; disconnection is handled elsewhere.
            }
        }
    }
    /**
     * Replay buffered events to a single client that reconnected.
     * Sends all events with seqId > lastEventId.
     */
    replayBufferedEvents(res, buffer, lastEventId) {
        let replayed = 0;
        for (const event of buffer) {
            if (event.seqId > lastEventId) {
                try {
                    res.write(`id: ${event.seqId}\n`);
                    res.write(`event: ${event.eventType}\n`);
                    res.write(`data: ${event.eventData}\n\n`);
                    replayed++;
                }
                catch {
                    break; // Client disconnected during replay
                }
            }
        }
        return replayed;
    }
}
exports.StreamProjector = StreamProjector;
//# sourceMappingURL=streamProjector.js.map