"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Buckets = void 0;
const loglevel_1 = __importDefault(require("loglevel"));
const pb = __importStar(require("@textile/buckets-grpc/buckets_pb"));
const buckets_pb_service_1 = require("@textile/buckets-grpc/buckets_pb_service");
const cids_1 = __importDefault(require("cids"));
const event_iterator_1 = require("event-iterator");
const grpc_web_1 = require("@improbable-eng/grpc-web");
const context_1 = require("@textile/context");
const threads_id_1 = require("@textile/threads-id");
const hub_threads_client_1 = require("@textile/hub-threads-client");
const normalize_1 = require("./normalize");
const logger = loglevel_1.default.getLogger('buckets');
/**
 * Buckets is a web-gRPC wrapper client for communicating with the web-gRPC enabled Textile Buckets API.
 * @example
 * Initialize a the Bucket API
 * ```typescript
 * import { Buckets, UserAuth } from '@textile/hub'
 *
 * const init = async (auth: UserAuth) => {
 *     const buckets = Buckets.withUserAuth(auth)
 *     return buckets
 * }
 * ```
 *
 * @example
 * Find an existing Bucket
 * ```typescript
 * import { Buckets } from '@textile/hub'
 *
 * const exists = async (buckets: Buckets, bucketName: string) => {
 *     const roots = await buckets.list();
 *     return roots.find((bucket) => bucket.name === bucketName)
 * }
 * ```
 */
class Buckets {
    /**
     * Creates a new gRPC client instance for accessing the Textile Buckets API.
     * @param context The context to use for interacting with the APIs. Can be modified later.
     */
    constructor(context = new context_1.Context()) {
        this.context = context;
        this.serviceHost = context.host;
        this.rpcOptions = {
            transport: context.transport,
            debug: context.debug,
        };
    }
    /**
     * Creates a new gRPC client instance for accessing the Textile Buckets API.
     * @param auth The user auth object.
     */
    static withUserAuth(auth, host = context_1.defaultHost, debug = false) {
        const context = context_1.Context.fromUserAuth(auth, host, debug);
        return new Buckets(context);
    }
    /**
     * Create a new gRPC client Bucket instance from a supplied key and secret
     * @param key The KeyInfo object containing {key: string, secret: string, type: 0}. 0 === User Group Key, 1 === Account Key
     */
    static withKeyInfo(key, host = context_1.defaultHost, debug = false) {
        return __awaiter(this, void 0, void 0, function* () {
            const context = new context_1.Context(host, debug);
            yield context.withKeyInfo(key);
            return new Buckets(context);
        });
    }
    /**
     * Create a new gRPC client Bucket instance from a preconfigured threads client
     * @param client a threads client
     */
    static fromClient(client) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Buckets(client.context);
        });
    }
    /**
     * Open a new / existing bucket by bucket name and ThreadID
     * @param name name of bucket
     * @param threadName the name of the thread where the bucket is stored (default 'buckets')
     * @param threadID id of thread where bucket is stored
     * @example
     * Initialize a Bucket called "app-name-files"
     * ```tyepscript
     * import { Buckets, UserAuth } from '@textile/hub'
     *
     * const open = async (auth: UserAuth, name: string) => {
     *     const buckets = Buckets.withUserAuth(auth)
     *     await buckets.open(name)
     *     return buckets
     * }
     * ```
     */
    open(name, threadName = 'buckets', threadID) {
        return __awaiter(this, void 0, void 0, function* () {
            if (threadID) {
                const id = threadID.toString();
                const client = new hub_threads_client_1.Client(this.context);
                const res = yield client.listThreads();
                const exists = res.listList.find((thread) => thread.id === id);
                if (!exists) {
                    yield client.newDB(threadID, threadName);
                }
                this.context.withThread(threadID.toString());
            }
            else {
                const client = new hub_threads_client_1.Client(this.context);
                try {
                    const res = yield client.getThread(threadName);
                    const existingId = typeof res.id === 'string' ? res.id : threads_id_1.ThreadID.fromBytes(res.id).toString();
                    this.context.withThread(existingId);
                }
                catch (error) {
                    if (error.message !== 'Thread not found') {
                        throw new Error(error.message);
                    }
                    const newId = threads_id_1.ThreadID.fromRandom();
                    yield client.newDB(newId, threadName);
                    this.context.withThread(newId.toString());
                }
            }
            const roots = yield this.list();
            const existing = roots.find((bucket) => bucket.name === name);
            if (existing) {
                return existing;
            }
            const created = yield this.init(name);
            return created.root;
        });
    }
    /**
     * Initializes a new bucket.
     * @public
     * @param name Human-readable bucket name. It is only meant to help identify a bucket in a UI and is not unique.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @example
     * Initialize a Bucket called "app-name-files"
     * ```tyepscript
     * import { Buckets } from '@textile/hub'
     *
     * const init = async (buckets: Buckets) => {
     *     return buckets.init("app-name-files")
     * }
     * ```
     */
    init(name, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('init request');
            const req = new pb.InitRequest();
            req.setName(name);
            const res = yield this.unary(buckets_pb_service_1.API.Init, req, ctx);
            return res.toObject();
        });
    }
    /**
     * Returns a list of all bucket roots.
     * @example
     * Find an existing Bucket named "app-name-files"
     * ```typescript
     * import { Buckets } from '@textile/hub'
     *
     * const exists = async (buckets: Buckets) => {
     *     const roots = await buckets.list();
     *     return roots.find((bucket) => bucket.name ===  "app-name-files")
     * }
     * ````
     */
    list(ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('list request');
            const req = new pb.ListRequest();
            const res = yield this.unary(buckets_pb_service_1.API.List, req, ctx);
            return res.toObject().rootsList;
        });
    }
    /**
     * Returns a list of bucket links.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @example
     * Generate the HTTP, IPNS, and IPFS links for a Bucket
     * ```tyepscript
     * import { Buckets } from '@textile/hub'
     *
     * const getLinks = async (buckets: Buckets) => {
     *    const links = buckets.links(bucketKey)
     *    return links.ipfs
     * }
     *
     * const getIpfs = async (buckets: Buckets) => {
     *    const links = buckets.links(bucketKey)
     *    return links.ipfs
     * }
     * ```
     */
    links(key, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('link request');
            const req = new pb.LinksRequest();
            req.setKey(key);
            const res = yield this.unary(buckets_pb_service_1.API.Links, req, ctx);
            return res.toObject();
        });
    }
    /**
     * Returns information about a bucket path.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param path A file/object (sub)-path within a bucket.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    listPath(key, path, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('list path request');
            const req = new pb.ListPathRequest();
            req.setKey(key);
            req.setPath(path);
            const res = yield this.unary(buckets_pb_service_1.API.ListPath, req, ctx);
            return res.toObject();
        });
    }
    /**
     * Removes an entire bucket. Files and directories will be unpinned.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    remove(key, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('remove request');
            const req = new pb.RemoveRequest();
            req.setKey(key);
            yield this.unary(buckets_pb_service_1.API.Remove, req, ctx);
            return;
        });
    }
    /**
     * Returns information about a bucket path.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param path A file/object (sub)-path within a bucket.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    removePath(key, path, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('remove path request');
            const req = new pb.RemovePathRequest();
            req.setKey(key);
            req.setPath(path);
            yield this.unary(buckets_pb_service_1.API.RemovePath, req, ctx);
            return;
        });
    }
    /**
     * Pushes a file to a bucket path.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param path A file/object (sub)-path within a bucket.
     * @param input The input file/stream/object.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @param opts Options to control response stream. Currently only supports a progress function.
     * @remarks
     * This will return the resolved path and the bucket's new root path.
     * @example
     * Push a file to the root of a bucket
     * ```tyepscript
     * import { Buckets } from '@textile/hub'
     *
     * const pushFile = async (content: string, bucketKey: string) => {
     *    const file = { path: '/index.html', content: Buffer.from(content) }
     *    return await buckets.pushPath(bucketKey!, 'index.html', file)
     * }
     * ```
     */
    pushPath(key, path, input, ctx, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
                var e_1, _a;
                // Only process the first  input if there are more than one
                const source = (yield normalize_1.normaliseInput(input).next()).value;
                const client = grpc_web_1.grpc.client(buckets_pb_service_1.API.PushPath, {
                    host: this.serviceHost,
                    transport: this.rpcOptions.transport,
                    debug: this.rpcOptions.debug,
                });
                client.onMessage((message) => {
                    var _a, _b, _c;
                    if (message.hasError()) {
                        // Reject on first error
                        reject(new Error(message.getError()));
                    }
                    else if (message.hasEvent()) {
                        const event = (_a = message.getEvent()) === null || _a === void 0 ? void 0 : _a.toObject();
                        if (event === null || event === void 0 ? void 0 : event.path) {
                            // @todo: Is there an standard library/tool for this step in JS?
                            const pth = event.path.startsWith('/ipfs/') ? event.path.split('/ipfs/')[1] : event.path;
                            const cid = new cids_1.default(pth);
                            const res = {
                                path: {
                                    path: `/ipfs/${cid.toString()}`,
                                    cid: cid,
                                    root: cid,
                                    remainder: '',
                                },
                                root: (_c = (_b = event.root) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : '',
                            };
                            resolve(res);
                        }
                        else if (opts === null || opts === void 0 ? void 0 : opts.progress) {
                            opts.progress(event === null || event === void 0 ? void 0 : event.bytes);
                        }
                    }
                    else {
                        reject(new Error('Invalid reply'));
                    }
                });
                client.onEnd((code) => {
                    if (code === grpc_web_1.grpc.Code.OK) {
                        resolve();
                    }
                    else {
                        reject(new Error(code.toString()));
                    }
                });
                if (source) {
                    const head = new pb.PushPathRequest.Header();
                    head.setPath(source.path || path);
                    head.setKey(key);
                    const req = new pb.PushPathRequest();
                    req.setHeader(head);
                    const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
                    client.start(metadata);
                    client.send(req);
                    if (source.content) {
                        try {
                            for (var _b = __asyncValues(source.content), _c; _c = yield _b.next(), !_c.done;) {
                                const chunk = _c.value;
                                const part = new pb.PushPathRequest();
                                part.setChunk(chunk);
                                client.send(part);
                            }
                        }
                        catch (e_1_1) { e_1 = { error: e_1_1 }; }
                        finally {
                            try {
                                if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
                            }
                            finally { if (e_1) throw e_1.error; }
                        }
                        client.finishSend();
                    }
                }
            }));
        });
    }
    /**
     * Pulls the bucket path, returning the bytes of the given file.
     * @param key Unique (IPNS compatible) identifier key for a bucket.
     * @param path A file/object (sub)-path within a bucket.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @param opts Options to control response stream. Currently only supports a progress function.
     */
    pullPath(key, path, ctx, opts) {
        const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
        const request = new pb.PullPathRequest();
        request.setKey(key);
        request.setPath(path);
        let written = 0;
        const events = new event_iterator_1.EventIterator(({ push, stop, fail }) => {
            const resp = grpc_web_1.grpc.invoke(buckets_pb_service_1.API.PullPath, {
                host: this.serviceHost,
                transport: this.rpcOptions.transport,
                debug: this.rpcOptions.debug,
                request,
                metadata,
                onMessage: (res) => __awaiter(this, void 0, void 0, function* () {
                    const chunk = res.getChunk_asU8();
                    push(chunk);
                    written += chunk.byteLength;
                    if (opts === null || opts === void 0 ? void 0 : opts.progress) {
                        opts.progress(written);
                    }
                }),
                onEnd: (status, message, _trailers) => __awaiter(this, void 0, void 0, function* () {
                    if (status !== grpc_web_1.grpc.Code.OK) {
                        fail(new Error(message));
                    }
                    stop();
                }),
            });
            return () => resp.close();
        });
        const it = Object.assign({ [Symbol.asyncIterator]() {
                return this;
            } }, events[Symbol.asyncIterator]());
        return it;
    }
    unary(methodDescriptor, req, ctx) {
        return new Promise((resolve, reject) => {
            const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
            grpc_web_1.grpc.unary(methodDescriptor, {
                request: req,
                host: this.serviceHost,
                transport: this.rpcOptions.transport,
                debug: this.rpcOptions.debug,
                metadata,
                onEnd: (res) => {
                    const { status, statusMessage, message } = res;
                    if (status === grpc_web_1.grpc.Code.OK) {
                        if (message) {
                            resolve(message);
                        }
                        else {
                            resolve();
                        }
                    }
                    else {
                        reject(new Error(statusMessage));
                    }
                },
            });
        });
    }
}
exports.Buckets = Buckets;
//# sourceMappingURL=buckets.js.map