"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionSession = void 0;
const protocol_1 = require("@streamr/protocol");
const Scaffold_1 = require("../utils/Scaffold");
const Signal_1 = require("../utils/Signal");
/**
 * Manages adding & removing subscriptions to node as needed.
 * A session contains one or more subscriptions to a single streamId + streamPartition pair.
 */
class SubscriptionSession {
    streamPartId;
    onRetired = Signal_1.Signal.once();
    isRetired = false;
    isStopped = false;
    subscriptions = new Set();
    pendingRemoval = new WeakSet();
    pipeline;
    node;
    constructor(streamPartId, messagePipelineFactory, node) {
        this.streamPartId = streamPartId;
        this.distributeMessage = this.distributeMessage.bind(this);
        this.node = node;
        this.onError = this.onError.bind(this);
        this.pipeline = messagePipelineFactory.createMessagePipeline({
            streamPartId
        });
        this.pipeline.onError.listen(this.onError);
        this.pipeline
            .pipe(this.distributeMessage)
            .onBeforeFinally.listen(async () => {
            if (!this.isStopped) {
                await this.stop();
            }
        });
        this.pipeline.flow();
    }
    async retire() {
        if (this.isRetired) {
            return;
        }
        this.isRetired = true;
        await this.onRetired.trigger();
    }
    async onError(error) {
        // eslint-disable-next-line promise/no-promise-in-callback
        await Promise.allSettled([...this.subscriptions].map(async (sub) => {
            await sub.handleError(error);
        }));
    }
    async *distributeMessage(src) {
        for await (const msg of src) {
            await Promise.all([...this.subscriptions].map(async (sub) => {
                await sub.push(msg);
            }));
            yield msg;
        }
    }
    onMessageInput = async (msg) => {
        if (!msg || this.isStopped || this.isRetired) {
            return;
        }
        if (msg.getStreamPartID() !== this.streamPartId) {
            return;
        }
        if (msg.messageType !== protocol_1.StreamMessageType.MESSAGE) {
            return;
        }
        const tasks = [];
        let hasNormalSubscriptions = false;
        for (const sub of this.subscriptions.values()) {
            if (sub.isRaw) {
                tasks.push(sub.push(msg));
            }
            else {
                hasNormalSubscriptions = true;
            }
        }
        if (hasNormalSubscriptions) {
            tasks.push(this.pipeline.push(msg));
        }
        await Promise.all(tasks);
    };
    async subscribe() {
        const node = await this.node.getNode();
        node.addMessageListener(this.onMessageInput);
        if (!node.isProxiedStreamPart(this.streamPartId)) {
            await node.join(this.streamPartId);
        }
        return node;
    }
    async unsubscribe(node) {
        this.pipeline.end();
        this.pipeline.return();
        this.pipeline.onError.end(new Error('done'));
        node.removeMessageListener(this.onMessageInput);
        await node.leave(this.streamPartId);
    }
    updateNodeSubscriptions = (() => {
        let node;
        return (0, Scaffold_1.Scaffold)([
            async () => {
                node = await this.subscribe();
                return async () => {
                    const prevNode = node;
                    node = undefined;
                    await this.unsubscribe(prevNode);
                    await this.stop();
                };
            },
        ], () => this.shouldBeSubscribed());
    })();
    async updateSubscriptions() {
        await this.updateNodeSubscriptions();
        if (!this.shouldBeSubscribed() && !this.isStopped) {
            await this.stop();
        }
    }
    shouldBeSubscribed() {
        return !this.isRetired && !this.isStopped && !!this.count();
    }
    async stop() {
        this.isStopped = true;
        this.pipeline.end();
        await this.retire();
        await this.pipeline.return();
    }
    has(sub) {
        return this.subscriptions.has(sub);
    }
    /**
     * Add subscription & appropriate connection handle.
     */
    async add(sub) {
        if (!sub || this.subscriptions.has(sub) || this.pendingRemoval.has(sub)) {
            return;
        } // already has
        this.subscriptions.add(sub);
        sub.onBeforeFinally.listen(() => {
            return this.remove(sub);
        });
        await this.updateSubscriptions();
    }
    /**
     * Remove subscription & appropriate connection handle.
     */
    async remove(sub) {
        if (!sub || this.pendingRemoval.has(sub) || !this.subscriptions.has(sub)) {
            return;
        }
        this.pendingRemoval.add(sub);
        this.subscriptions.delete(sub);
        try {
            if (!sub.isDone()) {
                await sub.unsubscribe();
            }
        }
        finally {
            await this.updateSubscriptions();
        }
    }
    /**
     * How many subscriptions
     */
    count() {
        return this.subscriptions.size;
    }
}
exports.SubscriptionSession = SubscriptionSession;
//# sourceMappingURL=SubscriptionSession.js.map