"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const env_paths_1 = __importDefault(require("env-paths"));
const path_1 = require("path");
const fs_1 = require("fs");
const sqlite_1 = require("sqlite");
const sqlite3_1 = __importDefault(require("sqlite3"));
const promises_1 = require("../promises");
const utils_1 = require("@streamr/utils");
class ServerPersistence {
    logger;
    dbFilePath;
    store;
    error;
    initCalled = false;
    migrationsPath;
    onInit;
    // uses createInstance factory pattern so that ServerPersistence and BrowserPersistence
    // are interchangeable
    static async createInstance(opts) {
        // TODO init() call could called here, so that we don't need to separate logic for 
        // initialization (i.e. check this.initCalled flag before eaach call).
        // It would be ok to do initialization, because the PersistenceManager already lazy loads
        // and therefore doesn't create this instance before it is needed
        return new ServerPersistence(opts);
    }
    constructor({ loggerFactory, clientId, migrationsPath, onInit }) {
        this.logger = loggerFactory.createLogger(module);
        const paths = (0, env_paths_1.default)('streamr-sdk');
        this.dbFilePath = (0, path_1.resolve)(paths.data, (0, path_1.join)('./', clientId, `GroupKeys.db`));
        this.migrationsPath = migrationsPath;
        this.onInit = onInit;
        this.init = (0, promises_1.pOnce)(this.init.bind(this));
    }
    async exists() {
        if (this.initCalled) {
            // wait for init if in progress
            await this.init();
        }
        try {
            await fs_1.promises.access(this.dbFilePath);
            return true;
        }
        catch (err) {
            if (err.code === 'ENOENT') {
                return false;
            }
            throw err;
        }
    }
    async tryExec(fn, maxRetries = 10, retriesLeft = maxRetries) {
        try {
            return await fn();
        }
        catch (err) {
            if (retriesLeft > 0 && err.code === 'SQLITE_BUSY') {
                this.logger.trace('Retry opening database after delay (database busy)', {
                    retryNo: maxRetries - retriesLeft + 1,
                    maxRetries
                });
                return this.tryExec(async () => {
                    // wait random time and retry
                    await (0, utils_1.wait)(10 + Math.random() * 500);
                    return fn();
                }, maxRetries, retriesLeft - 1);
            }
            throw err;
        }
    }
    async init() {
        this.initCalled = true;
        try {
            await fs_1.promises.mkdir((0, path_1.dirname)(this.dbFilePath), { recursive: true });
            // open the database
            const store = await (0, sqlite_1.open)({
                filename: this.dbFilePath,
                driver: sqlite3_1.default.Database
            });
            await this.tryExec(async () => {
                await store.configure('busyTimeout', 200);
                await store.run('PRAGMA journal_mode = WAL;');
            });
            if (this.migrationsPath !== undefined) {
                await this.tryExec(async () => {
                    try {
                        await store.migrate({
                            migrationsPath: this.migrationsPath
                        });
                    }
                    catch (err) {
                        if (err.code.startsWith('SQLITE_')) {
                            // ignore: some other migration is probably running, assume that worked
                            return;
                        }
                        throw err;
                    }
                });
            }
            await this.onInit?.(store);
            this.store = store;
        }
        catch (err) {
            this.logger.trace('Failed to open database', err);
            if (!this.error) {
                this.error = err;
            }
        }
        if (this.error) {
            throw this.error;
        }
        this.logger.trace('Initialized database');
    }
    async get(key, namespace) {
        if (!this.initCalled) {
            // can't have if doesn't exist
            if (!(await this.exists())) {
                return undefined;
            }
        }
        await this.init();
        const row = await this.store.get(`SELECT value_ FROM ${namespace} WHERE key_ = ?`, key);
        return row?.['value_'];
    }
    async set(key, value, namespace) {
        await this.init();
        await this.store.run(`INSERT INTO ${namespace} (key_, value_) VALUES ($key_, $value_) ON CONFLICT DO UPDATE SET value_ = $value_`, {
            $key_: key,
            $value_: value,
        });
    }
    async close() {
        if (!this.initCalled) {
            // nothing to close if never opened
            return;
        }
        await this.init();
        await this.store.close();
    }
}
exports.default = ServerPersistence;
//# sourceMappingURL=ServerPersistence.js.map