"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Stream = exports.VALID_FIELD_TYPES = void 0;
const protocol_1 = require("@streamr/protocol");
const utils_1 = require("@streamr/utils");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const range_1 = __importDefault(require("lodash/range"));
const Message_1 = require("./Message");
const StreamIDBuilder_1 = require("./StreamIDBuilder");
const StreamrClientError_1 = require("./StreamrClientError");
const Subscription_1 = require("./subscribe/Subscription");
const utils_2 = require("./utils/utils");
const waitForAssignmentsToPropagate_1 = require("./utils/waitForAssignmentsToPropagate");
exports.VALID_FIELD_TYPES = ['number', 'string', 'boolean', 'list', 'map'];
function getFieldType(value) {
    const type = typeof value;
    switch (true) {
        case Array.isArray(value): {
            return 'list';
        }
        case type === 'object': {
            return 'map';
        }
        case exports.VALID_FIELD_TYPES.includes(type): {
            // see https://github.com/microsoft/TypeScript/issues/36275
            return type;
        }
        default: {
            return undefined;
        }
    }
}
/**
 * A convenience API for managing and accessing an individual stream.
 *
 * @category Important
 */
/* eslint-disable no-underscore-dangle */
class Stream {
    id;
    metadata;
    _publisher;
    _subscriber;
    _resends;
    _streamRegistry;
    _streamStorageRegistry;
    _loggerFactory;
    _eventEmitter;
    _config;
    /** @internal */
    constructor(id, metadata, publisher, subscriber, resends, streamRegistry, streamStorageRegistry, loggerFactory, eventEmitter, config) {
        this.id = id;
        this.metadata = (0, utils_1.merge)({
            partitions: 1,
            // TODO should we remove this default or make config as a required StreamMetadata field?
            config: {
                fields: []
            }
        }, metadata);
        this._publisher = publisher;
        this._subscriber = subscriber;
        this._resends = resends;
        this._streamRegistry = streamRegistry;
        this._streamStorageRegistry = streamStorageRegistry;
        this._loggerFactory = loggerFactory;
        this._eventEmitter = eventEmitter;
        this._config = config;
    }
    /**
     * Updates the metadata of the stream by merging with the existing metadata.
     */
    async update(metadata) {
        const merged = (0, utils_1.merge)(this.getMetadata(), metadata);
        try {
            await this._streamRegistry.updateStream(this.id, merged);
        }
        finally {
            this._streamRegistry.clearStreamCache(this.id);
        }
        this.metadata = merged;
    }
    /**
     * Returns the partitions of the stream.
     */
    getStreamParts() {
        return (0, range_1.default)(0, this.getMetadata().partitions).map((p) => (0, protocol_1.toStreamPartID)(this.id, p));
    }
    /**
     * Returns the metadata of the stream.
     */
    getMetadata() {
        return this.metadata;
    }
    /**
     * Deletes the stream.
     *
     * @remarks Stream instance should not be used afterwards.
     */
    async delete() {
        try {
            await this._streamRegistry.deleteStream(this.id);
        }
        finally {
            this._streamRegistry.clearStreamCache(this.id);
        }
    }
    /**
     * Attempts to detect and update the {@link StreamMetadata.config} metadata of the stream by performing a resend.
     *
     * @remarks Only works on stored streams.
     *
     * @returns be mindful that in the case of there being zero messages stored, the returned promise will resolve even
     * though fields were not updated
     */
    async detectFields() {
        // Get last message of the stream to be used for field detecting
        const sub = await this._resends.resend((0, protocol_1.toStreamPartID)(this.id, StreamIDBuilder_1.DEFAULT_PARTITION), {
            last: 1
        }, (streamId) => this._streamStorageRegistry.getStorageNodes(streamId));
        const receivedMsgs = await (0, utils_1.collect)(sub);
        if (!receivedMsgs.length) {
            return;
        }
        const lastMessage = receivedMsgs[0].getParsedContent();
        const fields = Object.entries(lastMessage).map(([name, value]) => {
            const type = getFieldType(value);
            return !!type && {
                name,
                type,
            };
        }).filter(Boolean); // see https://github.com/microsoft/TypeScript/issues/30621
        // Save field config back to the stream
        await this.update({
            config: {
                fields
            }
        });
    }
    /**
     * Assigns the stream to a storage node.
     *
     * @category Important
     *
     * @param waitOptions - control how long to wait for storage node to pick up on assignment
     * @returns a resolved promise if (1) stream was assigned to storage node and (2) the storage node acknowledged the
     * assignment within `timeout`, otherwise rejects. Notice that is possible for this promise to reject but for the
     * storage node assignment to go through eventually.
     */
    async addToStorageNode(storageNodeAddress, waitOptions = {}) {
        const normalizedNodeAddress = (0, utils_1.toEthereumAddress)(storageNodeAddress);
        // check whether the stream is already stored: the assignment event listener logic requires that
        // there must not be an existing assignment (it timeouts if there is an existing assignment as the
        // storage node doesn't send an assignment event in that case)
        const isAlreadyStored = await this._streamStorageRegistry.isStoredStream(this.id, normalizedNodeAddress);
        if (isAlreadyStored) {
            return;
        }
        let assignmentSubscription;
        try {
            const streamPartId = (0, protocol_1.toStreamPartID)((0, utils_2.formStorageNodeAssignmentStreamId)(normalizedNodeAddress), StreamIDBuilder_1.DEFAULT_PARTITION);
            assignmentSubscription = new Subscription_1.Subscription(streamPartId, false, new eventemitter3_1.default(), this._loggerFactory);
            await this._subscriber.add(assignmentSubscription);
            const propagationPromise = (0, waitForAssignmentsToPropagate_1.waitForAssignmentsToPropagate)(assignmentSubscription, {
                id: this.id,
                partitions: this.getMetadata().partitions
            }, this._loggerFactory);
            await this._streamStorageRegistry.addStreamToStorageNode(this.id, normalizedNodeAddress);
            await (0, utils_1.withTimeout)(propagationPromise, 
            // eslint-disable-next-line no-underscore-dangle
            waitOptions.timeout ?? this._config._timeouts.storageNode.timeout, 'storage node did not respond');
        }
        finally {
            this._streamRegistry.clearStreamCache(this.id);
            await assignmentSubscription?.unsubscribe(); // should never reject...
        }
    }
    /**
     * See {@link StreamrClient.removeStreamFromStorageNode | StreamrClient.removeStreamFromStorageNode}.
     */
    async removeFromStorageNode(nodeAddress) {
        try {
            return this._streamStorageRegistry.removeStreamFromStorageNode(this.id, (0, utils_1.toEthereumAddress)(nodeAddress));
        }
        finally {
            this._streamRegistry.clearStreamCache(this.id);
        }
    }
    /**
     * See {@link StreamrClient.getStorageNodes | StreamrClient.getStorageNodes}.
     */
    async getStorageNodes() {
        return this._streamStorageRegistry.getStorageNodes(this.id);
    }
    /**
     * See {@link StreamrClient.publish | StreamrClient.publish}.
     *
     * @category Important
     */
    async publish(content, metadata) {
        const result = await this._publisher.publish(this.id, content, metadata);
        this._eventEmitter.emit('publish', undefined);
        return (0, Message_1.convertStreamMessageToMessage)(result);
    }
    /** @internal */
    static parseMetadata(metadata) {
        // TODO we could pick the fields of StreamMetadata explicitly, so that this
        // object can't contain extra fields
        if (metadata === '') {
            return {
                partitions: 1
            };
        }
        const err = new StreamrClientError_1.StreamrClientError(`Invalid stream metadata: ${metadata}`, 'INVALID_STREAM_METADATA');
        let json;
        try {
            json = JSON.parse(metadata);
        }
        catch (_ignored) {
            throw err;
        }
        if (json.partitions !== undefined) {
            try {
                (0, protocol_1.ensureValidStreamPartitionCount)(json.partitions);
                return json;
            }
            catch (_ignored) {
                throw err;
            }
        }
        else {
            return {
                ...json,
                partitions: 1
            };
        }
    }
    /**
     * See {@link StreamrClient.hasPermission | StreamrClient.hasPermission}.
     *
     * @category Important
     */
    async hasPermission(query) {
        return this._streamRegistry.hasPermission({
            streamId: this.id,
            ...query
        });
    }
    /**
     * See {@link StreamrClient.getPermissions | StreamrClient.getPermissions}.
     *
     * @category Important
     */
    async getPermissions() {
        return this._streamRegistry.getPermissions(this.id);
    }
    /**
     * See {@link StreamrClient.grantPermissions | StreamrClient.grantPermissions}.
     *
     * @category Important
     */
    async grantPermissions(...assignments) {
        return this._streamRegistry.grantPermissions(this.id, ...assignments);
    }
    /**
     * See {@link StreamrClient.revokePermissions | StreamrClient.revokePermissions}.
     *
     * @category Important
     */
    async revokePermissions(...assignments) {
        return this._streamRegistry.revokePermissions(this.id, ...assignments);
    }
}
exports.Stream = Stream;
//# sourceMappingURL=Stream.js.map