"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Resends = void 0;
const protocol_1 = require("@streamr/protocol");
const utils_1 = require("@streamr/utils");
const random_1 = __importDefault(require("lodash/random"));
const without_1 = __importDefault(require("lodash/without"));
const sample_1 = __importDefault(require("lodash/sample"));
const tsyringe_1 = require("tsyringe");
const Config_1 = require("../Config");
const StreamrClientError_1 = require("../StreamrClientError");
const StorageNodeRegistry_1 = require("../registry/StorageNodeRegistry");
const GeneratorUtils_1 = require("../utils/GeneratorUtils");
const LoggerFactory_1 = require("../utils/LoggerFactory");
const PushBuffer_1 = require("../utils/PushBuffer");
const PushPipeline_1 = require("../utils/PushPipeline");
const utils_2 = require("../utils/utils");
const MessagePipelineFactory_1 = require("./MessagePipelineFactory");
const trackerless_network_1 = require("@streamr/trackerless-network");
function isResendLast(options) {
    return options && typeof options === 'object' && 'last' in options && options.last != null;
}
function isResendFrom(options) {
    return options && typeof options === 'object' && 'from' in options && !('to' in options) && options.from != null;
}
function isResendRange(options) {
    return options && typeof options === 'object' && 'from' in options && 'to' in options && options.to && options.from != null;
}
const createUrl = (baseUrl, endpointSuffix, streamPartId, query = {}) => {
    const queryMap = {
        ...query,
        format: 'raw'
    };
    const [streamId, streamPartition] = protocol_1.StreamPartIDUtils.getStreamIDAndPartition(streamPartId);
    const queryString = (0, utils_2.createQueryString)(queryMap);
    return `${baseUrl}/streams/${encodeURIComponent(streamId)}/data/partitions/${streamPartition}/${endpointSuffix}?${queryString}`;
};
const getHttpErrorTransform = () => {
    return async (err) => {
        let message;
        if (err instanceof utils_2.FetchHttpStreamResponseError) {
            const body = await err.response.text();
            let descriptionSnippet;
            try {
                const json = JSON.parse(body);
                descriptionSnippet = `: ${json.error}`;
            }
            catch (err) {
                descriptionSnippet = '';
            }
            message = `Storage node fetch failed${descriptionSnippet}, httpStatus=${err.response.status}, url=${err.response.url}`;
        }
        else {
            message = err?.message ?? 'Unknown error';
        }
        return new StreamrClientError_1.StreamrClientError(message, 'STORAGE_NODE_ERROR');
    };
};
let Resends = class Resends {
    storageNodeRegistry;
    messagePipelineFactory;
    config;
    logger;
    /* eslint-disable indent */
    constructor(storageNodeRegistry, messagePipelineFactory, config, loggerFactory) {
        this.storageNodeRegistry = storageNodeRegistry;
        this.messagePipelineFactory = messagePipelineFactory;
        this.config = config;
        this.logger = loggerFactory.createLogger(module);
    }
    async resend(streamPartId, options, getStorageNodes, abortSignal) {
        const raw = options.raw ?? false;
        if (isResendLast(options)) {
            if (options.last <= 0) {
                const emptyStream = new PushPipeline_1.PushPipeline();
                emptyStream.endWrite();
                return emptyStream;
            }
            return this.fetchStream('last', streamPartId, {
                count: options.last
            }, raw, getStorageNodes, abortSignal);
        }
        else if (isResendRange(options)) {
            return this.fetchStream('range', streamPartId, {
                fromTimestamp: new Date(options.from.timestamp).getTime(),
                fromSequenceNumber: options.from.sequenceNumber,
                toTimestamp: new Date(options.to.timestamp).getTime(),
                toSequenceNumber: options.to.sequenceNumber,
                publisherId: options.publisherId !== undefined ? (0, utils_1.toEthereumAddress)(options.publisherId) : undefined,
                msgChainId: options.msgChainId
            }, raw, getStorageNodes, abortSignal);
        }
        else if (isResendFrom(options)) {
            return this.fetchStream('from', streamPartId, {
                fromTimestamp: new Date(options.from.timestamp).getTime(),
                fromSequenceNumber: options.from.sequenceNumber,
                publisherId: options.publisherId !== undefined ? (0, utils_1.toEthereumAddress)(options.publisherId) : undefined
            }, raw, getStorageNodes, abortSignal);
        }
        else {
            throw new StreamrClientError_1.StreamrClientError(`can not resend without valid resend options: ${JSON.stringify({ streamPartId, options })}`, 'INVALID_ARGUMENT');
        }
    }
    async fetchStream(resendType, streamPartId, query, raw, getStorageNodes, abortSignal) {
        const traceId = (0, utils_1.randomString)(5);
        this.logger.debug('Fetch resend data', {
            loggerIdx: traceId,
            resendType,
            streamPartId,
            query
        });
        const streamId = protocol_1.StreamPartIDUtils.getStreamID(streamPartId);
        const nodeAddresses = await getStorageNodes(streamId);
        if (!nodeAddresses.length) {
            throw new StreamrClientError_1.StreamrClientError(`no storage assigned: ${streamId}`, 'NO_STORAGE_NODES');
        }
        const nodeAddress = nodeAddresses[(0, random_1.default)(0, nodeAddresses.length - 1)];
        const nodeUrls = (await this.storageNodeRegistry.getStorageNodeMetadata(nodeAddress)).urls;
        const url = createUrl((0, sample_1.default)(nodeUrls), resendType, streamPartId, query);
        const messageStream = raw ? new PushPipeline_1.PushPipeline() : this.messagePipelineFactory.createMessagePipeline({
            streamPartId,
            /*
             * Disable ordering if the source of this resend is the only storage node. In that case there is no
             * other storage node from which we could fetch the gaps. When we set "disableMessageOrdering"
             * to true, we disable both gap filling and message ordering. As resend messages always arrive
             * in ascending order, we don't need the ordering functionality.
             */
            getStorageNodes: async () => (0, without_1.default)(nodeAddresses, nodeAddress),
            config: (nodeAddresses.length === 1) ? { ...this.config, orderMessages: false } : this.config
        });
        const lines = (0, GeneratorUtils_1.transformError)((0, utils_2.fetchLengthPrefixedFrameHttpBinaryStream)(url, abortSignal), getHttpErrorTransform());
        setImmediate(async () => {
            let count = 0;
            const messages = (0, GeneratorUtils_1.map)(lines, (bytes) => (0, trackerless_network_1.convertBytesToStreamMessage)(bytes));
            await (0, PushBuffer_1.pull)((0, GeneratorUtils_1.forEach)(messages, () => count++), messageStream);
            this.logger.debug('Finished resend', { loggerIdx: traceId, messageCount: count });
        });
        return messageStream;
    }
};
exports.Resends = Resends;
exports.Resends = Resends = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped),
    __param(0, (0, tsyringe_1.inject)((0, tsyringe_1.delay)(() => StorageNodeRegistry_1.StorageNodeRegistry))),
    __param(2, (0, tsyringe_1.inject)(Config_1.ConfigInjectionToken)),
    __metadata("design:paramtypes", [StorageNodeRegistry_1.StorageNodeRegistry,
        MessagePipelineFactory_1.MessagePipelineFactory, Object, LoggerFactory_1.LoggerFactory])
], Resends);
//# sourceMappingURL=Resends.js.map