"use strict";
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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable import/first */
// 'Hack' to get WebSocket in the global namespace on nodejs
;
global.WebSocket = require('isomorphic-ws');
const path_1 = __importDefault(require("path"));
const chai_1 = require("chai");
const threads_core_1 = require("@textile/threads-core");
const datastore_level_1 = __importDefault(require("datastore-level"));
const delay_1 = __importDefault(require("delay"));
const browser_or_node_1 = require("browser-or-node");
const interface_datastore_1 = require("interface-datastore");
const threads_store_1 = require("@textile/threads-store");
const threads_network_1 = require("@textile/threads-network");
const interface_datastore_2 = require("interface-datastore");
const db_1 = require("./db");
const eventbus_1 = require("./eventbus");
const utils_1 = require("./utils");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const level = require('level');
/**
 * Runs a complex db use-case, and returns events received according to the provided options.
 * @param los: The action path to listen on. Paths are structured as: <collection>.<id>.<type>,
 * and support wildcards. For example, <collection>.*.* will listen to all events for a given
 * collection. Double wildcard (**) is recursive, so <collection>.** will also listen to call
 * events on a collection. Similarly, ** listens to all events on all collections.
 */
function runListenersComplexUseCase(los) {
    return __awaiter(this, void 0, void 0, function* () {
        const store = new interface_datastore_2.MemoryDatastore();
        const db = new db_1.Database(store);
        const Collection1 = yield db.newCollectionFromObject('Collection1', {
            _id: '',
            name: '',
            counter: 0,
        });
        const Collection2 = yield db.newCollectionFromObject('Collection2', {
            _id: '',
            name: '',
            counter: 0,
        });
        // Create some instance *before* any listener, just to test that it doesn't appear on a
        // listener's "stream".
        const i1 = new Collection1({ _id: 'id-i1', name: 'Textile1' });
        yield i1.save();
        yield delay_1.default(500);
        const events = [];
        // @todo: Should we wrap this in a 'listen' method instead?
        for (const name of los) {
            db.on(name, (update) => {
                events.push(update);
            });
        }
        // Collection1 save i1
        i1.name = 'Textile0';
        yield i1.save();
        // Collection1 create i2
        const i2 = new Collection1({ _id: 'id-i2', name: 'Textile2' });
        yield i2.save();
        // Collection2 create j1
        const j1 = new Collection2({ _id: 'id-j1', name: 'Textile3' });
        yield j1.save();
        // Collection1 save i1
        // Collection1 save i2
        yield Collection1.writeTransaction((c) => __awaiter(this, void 0, void 0, function* () {
            i1.counter = 30;
            i2.counter = 11;
            yield c.save(i1, i2);
        }));
        // Collection2 save j1
        j1.counter = -1;
        j1.name = 'Textile33';
        yield j1.save();
        // Collection1 delete i1
        yield Collection1.delete(i1._id);
        // Collection2 delete j1 (use alternate API)
        yield j1.remove();
        // Collection2 delete i2
        yield Collection1.delete(i2._id);
        db.removeAllListeners();
        yield db.close();
        // Expected generated actions:
        // Collection1 Save i1
        // Collection1 Create i2
        // Collection2 Create j1
        // Save i1
        // Save i2
        // Collection2 Save j1
        // Delete i1
        // Collection2 Delete j1
        // Delete i2
        return events;
    });
}
describe('Database', () => {
    describe('end to end test', () => {
        it('should allow paired peers to exchange updates', function () {
            return __awaiter(this, void 0, void 0, function* () {
                if (browser_or_node_1.isBrowser)
                    return this.skip();
                // @todo This test is probably too slow for CI, but should run just fine locally
                // Should probably just skip it (https://stackoverflow.com/a/48121978) in CI
                // Peer 1: Create db1, register a collection, create and update an instance.
                // @note: No identity is supplied, so a random Ed25519 private key is used by default
                const d1 = new db_1.Database(new interface_datastore_2.MemoryDatastore());
                const ident1 = yield db_1.Database.randomIdentity();
                yield d1.start(ident1);
                const id1 = d1.threadID;
                if (id1 === undefined) {
                    throw new Error('should not be invalid thread id');
                }
                // Create a new collection
                const Dummy1 = yield d1.newCollectionFromObject('dummy', {
                    _id: '',
                    name: '',
                    counter: 0,
                });
                // Boilerplate to generate peer1 thread-addr
                const hostID = yield d1.network.getHostID();
                // const addrs = await d1.getInfo() // Normally we'd use this, but we're in Docker...
                const hostAddr = new threads_core_1.Multiaddr('/dns4/threads1/tcp/4006');
                const addr = utils_1.threadAddr(hostAddr, hostID.toB58String(), id1.toString());
                // Peer 2: Create a completely parallel db2, which will sync with the previous one and should
                // have the same state of dummy.
                const info = yield d1.network.getThread(id1);
                const datastore = new interface_datastore_2.MemoryDatastore();
                const client = new threads_network_1.Client(new threads_network_1.Provider('http://127.0.0.1:6207'));
                const network = new threads_network_1.Network(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('network')), client);
                const d2 = new db_1.Database(datastore, { network });
                const ident2 = yield db_1.Database.randomIdentity();
                yield d2.startFromAddress(ident2, addr, info.key);
                // Create parallel collection
                const Dummy2 = yield d2.newCollectionFromObject('dummy', {
                    _id: '',
                    name: '',
                    counter: 0,
                });
                const dummy1 = new Dummy1({ name: 'Textile', counter: 0 });
                dummy1.counter += 42;
                yield dummy1.save();
                yield delay_1.default(2000);
                const dummy2 = yield Dummy2.findById(dummy1._id);
                chai_1.expect(dummy2.name).to.equal(dummy1.name);
                chai_1.expect(dummy2.counter).to.equal(dummy1.counter);
                yield d1.close();
                yield d2.close();
            });
        }).timeout(5000);
    });
    describe('Persistence', () => {
        const tmp = path_1.default.join(__dirname, './test.db');
        after(() => {
            level.destroy(tmp, () => {
                return;
            });
        });
        it('should work with a persistent database and custom options', function () {
            return __awaiter(this, void 0, void 0, function* () {
                if (browser_or_node_1.isBrowser)
                    return this.skip();
                const datastore = new datastore_level_1.default(tmp);
                if (datastore)
                    yield datastore.db.clear();
                const dispatcher = new threads_store_1.Dispatcher(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('dispatcher')));
                const network = new threads_network_1.Network(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('network')), new threads_network_1.Client());
                const eventBus = new eventbus_1.EventBus(new threads_store_1.DomainDatastore(datastore, new interface_datastore_1.Key('eventbus')), network);
                const db = new db_1.Database(datastore, { dispatcher, network, eventBus });
                const id = threads_core_1.ThreadID.fromRandom(threads_core_1.ThreadID.Variant.Raw, 32);
                yield db.start(yield db_1.Database.randomIdentity(), { threadID: id });
                yield db.newCollectionFromObject('dummy', {
                    _id: '',
                    name: '',
                    counter: 0,
                });
                // Re-do again to re-use id. If something wasn't closed correctly, would fail
                yield db.close(); // Closes eventBus and datastore
                yield db.start(yield db_1.Database.randomIdentity(), { threadID: id });
                chai_1.expect(db.collections.size).to.equal(1);
                yield db.close();
            });
        });
    });
    describe('Events', () => {
        const assertEvents = (events, expected) => {
            chai_1.expect(events).to.have.length(expected.length);
            for (const i in events) {
                chai_1.expect(events[i]).to.deep.include(expected[i]);
            }
        };
        it('should listen to all db events', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase(['**']);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Create, id: 'id-i2' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Create, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i2' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Save, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Delete, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i2' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to events on collection 1 only', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase(['Collection1.**']);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Create, id: 'id-i2' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i2' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i2' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to events on collection 2 only', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase(['Collection2.**']);
            const expected = [
                { collection: 'Collection2', type: threads_store_1.Op.Type.Create, id: 'id-j1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Save, id: 'id-j1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Delete, id: 'id-j1' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to any create events', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase([`**.${threads_store_1.Op.Type.Create}`]);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Create, id: 'id-i2' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Create, id: 'id-j1' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to any save events', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase([`**.${threads_store_1.Op.Type.Save}`]);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i2' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Save, id: 'id-j1' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to any delete events', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase([`**.${threads_store_1.Op.Type.Delete}`]);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Delete, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i2' },
            ];
            assertEvents(actions, expected);
        }));
        it('should listen to any collection1 events or delete events on collection2', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase([
                'Collection1.**',
                `Collection2.*.${threads_store_1.Op.Type.Delete}`,
            ]);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Create, id: 'id-i2' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i2' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Delete, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i2' },
            ];
            assertEvents(actions, expected);
        }));
        it('should not listen to any events for non-existant collections', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase(['Collection3.**']);
            const expected = [];
            assertEvents(actions, expected);
        }));
        it('should listen to various complex mixed event types', () => __awaiter(void 0, void 0, void 0, function* () {
            const actions = yield runListenersComplexUseCase([
                `Collection2.*.${threads_store_1.Op.Type.Save}`,
                `Collection1.id-i2.${threads_store_1.Op.Type.Save}`,
                `Collection1.id-i1.${threads_store_1.Op.Type.Delete}`,
                `Collection2.id-j1.${threads_store_1.Op.Type.Delete}`,
            ]);
            const expected = [
                { collection: 'Collection1', type: threads_store_1.Op.Type.Save, id: 'id-i2' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Save, id: 'id-j1' },
                { collection: 'Collection1', type: threads_store_1.Op.Type.Delete, id: 'id-i1' },
                { collection: 'Collection2', type: threads_store_1.Op.Type.Delete, id: 'id-j1' },
            ];
            assertEvents(actions, expected);
        }));
    });
    describe('Basic', () => {
        const tmp = path_1.default.join(__dirname, './test.db');
        after(() => {
            level.destroy(tmp, (_err) => {
                return;
            });
        });
        it('should return valid addrs and keys for sharing', () => __awaiter(void 0, void 0, void 0, function* () {
            var _a;
            const store = new interface_datastore_2.MemoryDatastore();
            const db = new db_1.Database(store);
            const threadID = threads_core_1.ThreadID.fromRandom();
            yield db.start(yield db_1.Database.randomIdentity(), { threadID });
            const info = yield db.getInfo();
            chai_1.expect((_a = info === null || info === void 0 ? void 0 : info.addrs) === null || _a === void 0 ? void 0 : _a.size).to.be.greaterThan(1);
            chai_1.expect(info === null || info === void 0 ? void 0 : info.key).to.not.be.undefined;
            yield db.close();
        }));
        it('should automatically open if not yet "opened"', function () {
            return __awaiter(this, void 0, void 0, function* () {
                if (browser_or_node_1.isBrowser)
                    return this.skip();
                const child = new datastore_level_1.default(tmp);
                const db = new db_1.Database(child);
                const Col = yield db.newCollectionFromObject('blah', { _id: '' });
                chai_1.expect(Col).to.not.be.undefined;
                yield db.close();
            });
        });
        it('should throw if our database and thread id do not match', function () {
            return __awaiter(this, void 0, void 0, function* () {
                const store = new interface_datastore_2.MemoryDatastore();
                let db = new db_1.Database(store);
                const ident = yield db_1.Database.randomIdentity();
                yield db.start(ident, { threadID: threads_core_1.ThreadID.fromRandom() });
                yield db.close();
                // Now 'reopen' the database
                db = new db_1.Database(store);
                try {
                    yield db.start(ident, { threadID: threads_core_1.ThreadID.fromRandom() });
                    throw new Error('should have throw');
                }
                catch (err) {
                    chai_1.expect(err).to.equal(db_1.mismatchError);
                }
                yield db.close();
            });
        });
    });
});
//# sourceMappingURL=db.spec.js.map