"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sleep = void 0;
const arbundles_1 = require("arbundles");
const utils_1 = __importDefault(require("./utils"));
const dist_1 = __importDefault(require("@supercharge/promise-pool/dist"));
const async_retry_1 = __importDefault(require("async-retry"));
const stream_1 = require("stream");
const chunker_1 = __importDefault(require("./chunker"));
const crypto_1 = __importDefault(require("crypto"));
// import mime from "mime-types";
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
exports.sleep = sleep;
// eslint-disable-next-line @typescript-eslint/naming-convention
class Uploader {
    constructor(api, utils, currency, currencyConfig) {
        this.api = api;
        this.currency = currency;
        this.currencyConfig = currencyConfig;
        this.utils = utils;
    }
    /**
     * Uploads data to the bundler
     * @param data
     * @param tags
     * @returns the response from the bundler
     */
    async upload(data, tags) {
        const signer = await this.currencyConfig.getSigner();
        const dataItem = (0, arbundles_1.createData)(data, signer, { tags });
        await dataItem.sign(signer);
        return await this.transactionUploader(dataItem);
    }
    /**
     * Uploads a given transaction to the bundler
     * @param transaction
     */
    async transactionUploader(transaction) {
        let res;
        const length = transaction.getRaw().byteLength;
        if (this.forceUseChunking || length > 190000000) {
            res = await this.chunkedTransactionUploader(stream_1.Readable.from(transaction.getRaw()), transaction.id, length);
        }
        else {
            const { protocol, host, port, timeout } = this.api.getConfig();
            res = await this.api.post(`${protocol}://${host}:${port}/tx/${this.currency}`, transaction.getRaw(), {
                headers: { "Content-Type": "application/octet-stream" },
                timeout,
                maxBodyLength: Infinity
            });
        }
        switch (res.status) {
            case 201:
                res.data = { id: transaction.id };
                return res;
            case 402:
                throw new Error("Not enough funds to send data");
            default:
                if (res.status >= 400) {
                    throw new Error(`whilst uploading DataItem: ${res.status} ${res.statusText}`);
                }
        }
        return res;
    }
    async concurrentUploader(data, concurrency = 5, resultProcessor, logFunction) {
        const errors = [];
        const results = await dist_1.default
            .for(data)
            .withConcurrency(concurrency >= 1 ? concurrency : 5)
            .handleError(async (error, _) => {
            errors.push(error);
            if (error.message === "Not enough funds to send data") {
                throw error;
            }
        })
            .process(async (item, i, _) => {
            await (0, async_retry_1.default)(async (bail) => {
                try {
                    const res = await this.processItem(item);
                    if (i % concurrency == 0) {
                        await logFunction(`Processed ${i} Items`);
                    }
                    if (resultProcessor) {
                        return await resultProcessor({ item, res, i });
                    }
                    else {
                        return { item, res, i };
                    }
                }
                catch (e) {
                    if (e.message === "Not enough funds to send data") {
                        bail(e);
                    }
                    throw e;
                }
            }, { retries: 3, minTimeout: 1000, maxTimeout: 10000 });
        });
        return { errors, results: results.results };
    }
    async processItem(item) {
        if (typeof item === "string") {
            item = Buffer.from(item);
        }
        if (Buffer.isBuffer(item)) {
            const signer = await this.currencyConfig.getSigner();
            item = (0, arbundles_1.createData)(item, signer, { anchor: crypto_1.default.randomBytes(32).toString("base64").slice(0, 32) });
            await item.sign(signer);
        }
        return await this.transactionUploader(item);
    }
    /**
     * geneates a manifest JSON object
     * @param config.items mapping of logical paths to item IDs
     * @param config.indexFile optional logical path of the index file for the manifest
     * @returns
     */
    async generateManifest(config) {
        const { items, indexFile } = config;
        const manifest = {
            manifest: "arweave/paths",
            version: "0.1.0",
            paths: {}
        };
        if (indexFile) {
            if (!items.has(indexFile)) {
                throw new Error(`Unable to access item: ${indexFile}`);
            }
            manifest["index"] = { path: indexFile };
        }
        for (const [k, v] of items.entries()) {
            manifest.paths[k] = { id: v };
        }
        return manifest;
    }
    /**
     * Chunking data uploader
     * @param dataStream - Readble of a pre-signed dataItem
     * @param id - the ID of the dataItem
     * @param size - the size of the dataItem
     * @param chunkSize - optional size to chunk the file - min 100_000, max 190_000_000 (in bytes)
     * @param batchSize - number of chunks to concurrently upload
     */
    async chunkedTransactionUploader(dataStream, id, size, chunkSize = 25000000, batchSize = 5) {
        if (chunkSize < 1000000 || chunkSize > 190000000) {
            throw new Error("Invalid chunk size - must be betweem 100,000 and 190,000,000 bytes");
        }
        if (batchSize < 1) {
            throw new Error("batch size too small! must be >=1");
        }
        const promiseFactory = (d, o) => {
            return new Promise((r, e) => {
                (0, async_retry_1.default)(async () => {
                    this.api.post(`/chunks/${this.currency}/${id}/${o}`, d, {
                        headers: { "Content-Type": "application/octet-stream" },
                        maxBodyLength: Infinity,
                        maxContentLength: Infinity,
                    }).then(re => r({ o, d: re })).catch(er => e({ o, e: er }));
                }),
                    { retries: 3, minTimeout: 1000, maxTimeout: 10000 };
            });
        };
        const getres = await this.api.get(`/chunks/${this.currency}/${id}/${size}`);
        await utils_1.default.checkAndThrow(getres, "Getting chunk info");
        const present = getres.data.map(v => +v);
        const remainder = size % chunkSize;
        const chunks = (size - remainder) / chunkSize;
        const missing = [];
        for (let i = 0; i < chunks + 1; i++) {
            const s = i * chunkSize;
            if (!present.includes(s)) {
                missing.push(s);
            }
        }
        // console.log(missing);
        let offset = 0;
        const processing = [];
        const ckr = new chunker_1.default({
            chunkSize: chunkSize,
            flushTail: true
        });
        dataStream.pipe(ckr);
        for await (const chunk of ckr) {
            const data = chunk.data;
            if (chunk.id % batchSize == 0) {
                await Promise.allSettled(processing);
            }
            // console.log(`posting chunk ${chunk.id} - ${offset} (${offset + data.length})`)
            if (missing.includes(offset)) {
                processing.push(promiseFactory(data, offset));
            }
            offset += data.length;
        }
        await Promise.allSettled(processing);
        const finishUpload = await this.api.post(`/chunks/${this.currency}/${id}/-1`, null, {
            headers: { "Content-Type": "application/octet-stream" },
            timeout: this.api.config.timeout * 10 // server side reconstruction can take a while
        });
        if (finishUpload.status === 402) {
            throw new Error("Not enough funds to send data");
        }
        // this will throw if the dataItem reconstruction fails
        await utils_1.default.checkAndThrow(finishUpload, "Finalising upload");
        return finishUpload;
    }
    set useChunking(state) {
        if (typeof state === "boolean") {
            this.forceUseChunking = state;
        }
    }
    set contentType(type) {
        // const fullType = mime.contentType(type)
        // if(!fullType){
        //     throw new Error("Invali")
        // }
        this.contentTypeOverride = type;
    }
}
exports.default = Uploader;
//# sourceMappingURL=upload.js.map