"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 __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.Database = exports.missingIdentity = exports.mismatchError = void 0;
const to_json_schema_1 = __importDefault(require("to-json-schema"));
const cbor_sync_1 = __importDefault(require("cbor-sync"));
const threads_network_1 = require("@textile/threads-network");
const context_1 = require("@textile/context");
const eventemitter2_1 = require("eventemitter2");
const threads_store_1 = require("@textile/threads-store");
const interface_datastore_1 = require("interface-datastore");
const threads_core_1 = require("@textile/threads-core");
const datastore_level_1 = __importDefault(require("datastore-level"));
const eventbus_1 = require("./eventbus");
const collection_1 = require("./collection");
const utils_1 = require("./utils");
const metaKey = new interface_datastore_1.Key('meta');
const schemaKey = metaKey.child(new interface_datastore_1.Key('schema'));
const duplicateCollection = new Error('Duplicate collection');
exports.mismatchError = new Error('Input ThreadID does not match existing database ThreadID. Consider creating new database or use a matching ThreadID.');
exports.missingIdentity = new Error('Identity required. You may use Database.randomIdentity() to generate a new one, but see caveats in docs.');
class Database {
    /**
     * Database creates a new database using the provided thread.
     * @param datastore The primary datastore or a name for a datastore.
     * It is used to partition out "sub-domains" for collections.
     * @param options A set of database options.
     * These are used to control the operation of the underlying database.
     */
    constructor(store, options = {}) {
        var _a, _b;
        /**
         * Collections is a map of collections (database tables)
         */
        this.collections = new Map();
        /**
         * The event emitter that can be used by subscribers to follow database updates.
         * Event names are structured as <collection>.<id>.<type>, and they support 'wildcard'
         * matching, so `emitter.many(['foo', '*', Op.Type.Delete], callback)` will match all
         * delete operations on the 'foo' collection. Similarly, `emitter.on('foo.**', callback)` will
         * match all event types on the 'foo' collection. To observe a given instance, try
         * `emitter.on('foo.${instance._id}', callback)`. See EventEmitter2 docs for further details.
         */
        this.emitter = new eventemitter2_1.EventEmitter2({ wildcard: true });
        const datastore = typeof store === 'string' ? new datastore_level_1.default(store) : store;
        this.child = new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('db'));
        this.dispatcher = (_a = options.dispatcher) !== null && _a !== void 0 ? _a : new threads_store_1.Dispatcher(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('dispatcher')));
        if (options.network === undefined) {
            const client = new threads_network_1.Client();
            this.network = new threads_network_1.Network(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('network')), client);
        }
        else {
            this.network = options.network;
        }
        this.eventBus = (_b = options.eventBus) !== null && _b !== void 0 ? _b : new eventbus_1.EventBus(new threads_store_1.DomainDatastore(this.child, new interface_datastore_1.Key('eventbus')), this.network);
    }
    /**
     * Create a new network connected instance from a supplied user auth object.
     * @param auth The user auth object.
     */
    static withUserAuth(auth, store, options, host = context_1.defaultHost, debug = false) {
        const context = context_1.Context.fromUserAuth(auth, host, debug);
        const client = new threads_network_1.Client(context);
        const network = new threads_network_1.Network(store, client);
        const opts = Object.assign(Object.assign({}, options), { network });
        return new Database(store, opts);
    }
    ownLogInfo() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.threadID) {
                const info = yield this.network.getThread(this.threadID);
                return this.network.getOwnLog(info, false);
            }
        });
    }
    /**
     * newCollectionFromObject creates a new collection from an initial input object.
     * It will attempt to infer the JSON schema from the input object.
     * @param name A name for the collection.
     * @param data A valid JSON object.
     */
    newCollectionFromObject(name, data) {
        const schema = to_json_schema_1.default(data);
        return this.newCollection(name, schema);
    }
    /**
     * newCollection creates a new empty collection from an input JSON schema.
     * @param name A name for the collection.
     * @param schema A valid JSON schema object.
     */
    newCollection(name, schema) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.collections.has(name)) {
                throw duplicateCollection;
            }
            const key = schemaKey.instance(name);
            const exists = yield this.child.has(key);
            if (!exists) {
                yield this.child.put(key, cbor_sync_1.default.encode(schema));
            }
            const { dispatcher, child } = this;
            const collection = new collection_1.Collection(name, schema, { child, dispatcher });
            collection.child.on('update', this.onUpdate.bind(this));
            collection.child.on('events', this.onEvents.bind(this));
            this.collections.set(name, collection);
            return collection;
        });
    }
    /**
     * Open (and sync) a remote database.
     * Opens the underlying datastore if not already open, and enables the dispatcher and
     * underlying services (event bus, network network, etc). This method will also begin pulling
     * from the underlying remote Thread. Only one of `start` or `startFromAddress` should be used.
     * @param identity An identity to use for creating records in the database. A random identity
     * can be created with `Database.randomIdentity()`, however, it is not easy/possible to migrate
     * identities after the fact. Please store or otherwise persist any identity information
     * if you wish to retrieve user data later, or use an external identity provider.
     * @param addr The address for the thread with which to connect.
     * Should be of the form /ip4/<url/ip-address>/tcp/<port>/p2p/<peer-id>/thread/<thread-id>
     * @param threadKey Set of symmetric keys.
     * @param opts A set of options to configure the setup and usage of the underlying database.
     */
    startFromAddress(identity, addr, threadKey, opts = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            if (identity === undefined) {
                throw exports.missingIdentity;
            }
            yield this.network.getToken(identity);
            yield this.child.open();
            const idKey = metaKey.child(new interface_datastore_1.Key('threadid'));
            const hasExisting = yield this.child.has(idKey);
            if (hasExisting) {
                throw exports.mismatchError;
            }
            // @todo: When adding a new thread, use identity for log key...
            const info = yield this.network.addThread(addr, { threadKey });
            yield this.child.put(idKey, info.id.toBytes());
            this.threadID = info.id;
            for (const { name, schema } of (opts.collections || []).values()) {
                yield this.newCollection(name, schema);
            }
            yield this.eventBus.start(this.threadID);
            this.eventBus.on('record', this.onRecord.bind(this));
            if (this.threadID)
                this.network.pullThread(this.threadID); // Don't await
        });
    }
    /**
     * Open (and start) the database.
     * Opens the underlying datastore if not already open, and enables the dispatcher and
     * underlying services (event bus, network network, etc).
     * @param identity An identity to use for creating records in the database. A random identity
     * can be created with `Database.randomIdentity()`, however, it is not easy/possible to migrate
     * identities after the fact. Please store or otherwise persist any identity information
     * if you wish to retrieve user data later, or use an external identity provider.
     * @param opts A set of options to configure the setup and usage of the underlying database.
     */
    start(identity, opts = {}) {
        return __awaiter(this, void 0, void 0, function* () {
            if (identity === undefined) {
                throw exports.missingIdentity;
            }
            yield this.network.getToken(identity);
            yield this.child.open();
            const idKey = metaKey.child(new interface_datastore_1.Key('threadid'));
            const hasExisting = yield this.child.has(idKey);
            if (opts.threadID === undefined) {
                if (hasExisting) {
                    this.threadID = threads_core_1.ThreadID.fromBytes(yield this.child.get(idKey));
                }
                else {
                    // @todo: When creating a new thread, use identity for log key...
                    const info = yield utils_1.createThread(this.network);
                    yield this.child.put(idKey, info.id.toBytes());
                    this.threadID = info.id;
                }
            }
            else {
                if (hasExisting) {
                    const existing = threads_core_1.ThreadID.fromBytes(yield this.child.get(idKey));
                    if (!existing.equals(opts.threadID)) {
                        throw exports.mismatchError;
                    }
                    this.threadID = existing;
                }
                else {
                    let info;
                    try {
                        info = yield this.network.getThread(opts.threadID);
                    }
                    catch (_err) {
                        info = yield utils_1.createThread(this.network, opts.threadID);
                    }
                    yield this.child.put(idKey, info.id.toBytes());
                    this.threadID = info.id;
                }
            }
            yield this.rehydrate();
            // Now that we have re-hydrated any existing collections, add the ones specified here
            for (const { name, schema } of (opts.collections || []).values()) {
                yield this.newCollection(name, schema);
            }
            yield this.eventBus.start(this.threadID);
            this.eventBus.on('record', this.onRecord.bind(this));
            if (this.threadID)
                this.network.pullThread(this.threadID); // Don't await
        });
    }
    /**
     * Return db (remote) address and keys.
     */
    getInfo() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.threadID !== undefined) {
                const info = yield this.network.getThread(this.threadID);
                return {
                    key: info.key,
                    addrs: info.addrs,
                };
            }
        });
    }
    /**
     * Close and stop the database.
     * Stops the underlying datastore if not already stopped, and disables the dispatcher and
     * underlying services (event bus, network network, etc.)
     */
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            this.collections.clear();
            yield this.eventBus.stop();
            return this.child.close();
        });
    }
    onRecord(rec) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            if ((_a = this.threadID) === null || _a === void 0 ? void 0 : _a.equals(rec.threadID)) {
                // @todo should just cache this information, as its unlikely to change
                const info = yield this.network.getThread(this.threadID);
                const logInfo = yield this.network.getOwnLog(info, false);
                if (logInfo === null || logInfo === void 0 ? void 0 : logInfo.id.equals(rec.logID)) {
                    return; // Ignore our own events since DB already dispatches to DB reducers
                }
                const value = yield utils_1.decodeRecord(rec, info);
                if (value !== undefined) {
                    const collection = this.collections.get(value.collection);
                    if (collection !== undefined) {
                        const key = collection.child.prefix.child(new interface_datastore_1.Key(value.id));
                        yield this.dispatcher.dispatch({ key, value });
                    }
                }
            }
        });
    }
    onEvents(...events) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            const id = (_a = this.threadID) === null || _a === void 0 ? void 0 : _a.toBytes();
            if (id !== undefined) {
                for (const body of events) {
                    yield this.eventBus.push({ id, body });
                }
            }
        });
    }
    onUpdate(...updates) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const update of updates) {
                // Event name: <collection>.<id>.<type>
                const event = [update.collection, update.id];
                if (update.type !== undefined) {
                    event.push(update.type.toString());
                }
                this.emitter.emit(event, update);
            }
        });
    }
    rehydrate() {
        var e_1, _a;
        return __awaiter(this, void 0, void 0, function* () {
            const it = this.child.query({ prefix: schemaKey.toString() });
            try {
                for (var it_1 = __asyncValues(it), it_1_1; it_1_1 = yield it_1.next(), !it_1_1.done;) {
                    const { key, value } = it_1_1.value;
                    yield this.newCollection(key.name(), cbor_sync_1.default.decode(value));
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (it_1_1 && !it_1_1.done && (_a = it_1.return)) yield _a.call(it_1);
                }
                finally { if (e_1) throw e_1.error; }
            }
        });
    }
    /**
     * Create a random user identity.
     */
    static randomIdentity() {
        return __awaiter(this, void 0, void 0, function* () {
            return threads_core_1.Libp2pCryptoIdentity.fromRandom();
        });
    }
}
__decorate([
    utils_1.Cache({ duration: 1800000 }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], Database.prototype, "ownLogInfo", null);
exports.Database = Database;
//# sourceMappingURL=db.js.map