"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "CentralServerConnection", {
    enumerable: true,
    get: function() {
        return CentralServerConnection;
    }
});
const _config = /*#__PURE__*/ _interop_require_default(require("config"));
const _apiclient = require("@tamanu/api-client");
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _selectFacilityIds = require("@tamanu/utils/selectFacilityIds");
const _logging = require("@tamanu/shared/services/logging");
const _serverInfo = require("../serverInfo");
function _check_private_redeclaration(obj, privateCollection) {
    if (privateCollection.has(obj)) {
        throw new TypeError("Cannot initialize the same private elements twice on an object");
    }
}
function _class_apply_descriptor_get(receiver, descriptor) {
    if (descriptor.get) {
        return descriptor.get.call(receiver);
    }
    return descriptor.value;
}
function _class_apply_descriptor_set(receiver, descriptor, value) {
    if (descriptor.set) {
        descriptor.set.call(receiver, value);
    } else {
        if (!descriptor.writable) {
            throw new TypeError("attempted to set read only private field");
        }
        descriptor.value = value;
    }
}
function _class_extract_field_descriptor(receiver, privateMap, action) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to " + action + " private field on non-instance");
    }
    return privateMap.get(receiver);
}
function _class_private_field_get(receiver, privateMap) {
    var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
    return _class_apply_descriptor_get(receiver, descriptor);
}
function _class_private_field_init(obj, privateMap, value) {
    _check_private_redeclaration(obj, privateMap);
    privateMap.set(obj, value);
}
function _class_private_field_set(receiver, privateMap, value) {
    var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set");
    _class_apply_descriptor_set(receiver, descriptor, value);
    return value;
}
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
var _loginData = /*#__PURE__*/ new WeakMap();
let CentralServerConnection = class CentralServerConnection extends _apiclient.TamanuApi {
    async fetch(endpoint, options = {}, upOptions = null) {
        let retryAuth;
        let query;
        let config;
        if (!upOptions || options.query || options.retryAuth || options.method) {
            // this is a local style 2-argument call
            retryAuth = options.retryAuth ?? true;
            query = options.query ?? {};
            delete options.retryAuth;
            delete options.query;
            config = options;
        } else {
            // this is an api-client style 3-argument call
            retryAuth = upOptions.retryAuth ?? false;
            delete upOptions.retryAuth;
            query = options;
            config = upOptions;
        }
        if ([
            'login',
            'refresh'
        ].includes(endpoint)) {
            retryAuth = false;
        }
        if (retryAuth && !this.hasToken()) {
            await this.connect(options.preserveBackoffForAuthAttempt ? options.backoff : undefined);
        }
        try {
            return await super.fetch(endpoint, query, config);
        } catch (error) {
            if (retryAuth && error.type?.startsWith(_errors.ERROR_TYPE.AUTH)) {
                await this.connect();
                return await super.fetch(endpoint, query, config);
            }
            throw error;
        }
    }
    async pollUntilTrue(endpoint) {
        return this.pollUntilOk(endpoint);
    }
    async connect(backoff = _config.default.sync.backoff, timeout = this.timeout) {
        try {
            await this.refreshToken({
                retryAuth: false
            });
            return;
        } catch (_) {
        // ignore error
        }
        const { email, password } = _config.default.sync;
        _logging.log.info(`Logging in to ${this.host} as ${email}...`);
        return await this.login(email, password, {
            backoff,
            timeout,
            scopes: [
                _constants.DEVICE_SCOPES.SYNC_CLIENT
            ]
        }).then((loginData)=>{
            return _class_private_field_set(this, _loginData, loginData);
        });
    }
    async loginData() {
        if (!this.hasToken() || !_class_private_field_get(this, _loginData)) {
            await this.connect();
        }
        return _class_private_field_get(this, _loginData);
    }
    async streaming() {
        return Boolean((await this.loginData())?.settings?.sync?.streaming?.enabled);
    }
    async startSyncSession({ urgent, lastSyncedTick }) {
        const facilityIds = (0, _selectFacilityIds.selectFacilityIds)(_config.default);
        const { sessionId, status } = await this.fetch('sync', {
            method: 'POST',
            body: {
                facilityIds,
                deviceId: this.deviceId,
                urgent,
                lastSyncedTick
            }
        });
        if (!sessionId) {
            // we're waiting in a queue
            return {
                status
            };
        }
        // then, wait until the sync session is ready
        // this is because POST /sync (especially the tickTockGlobalClock action) might get blocked
        // and take a while if the central server is concurrently persisting records from another client
        if (await this.streaming()) {
            for await (const { kind, message } of this.stream(()=>({
                    endpoint: `sync/${sessionId}/ready/stream`
                }))){
                handler: switch(kind){
                    case _constants.SYNC_STREAM_MESSAGE_KIND.SESSION_WAITING:
                        break handler;
                    case _constants.SYNC_STREAM_MESSAGE_KIND.END:
                        // includes the new tick from starting the session
                        return {
                            sessionId,
                            ...message
                        };
                    default:
                        _logging.log.warn(`Unexpected message kind: ${kind}`);
                }
            }
            throw new Error('Unexpected end of stream');
        }
        await this.pollUntilTrue(`sync/${sessionId}/ready`);
        // when polling, we need to separately fetch the new tick from starting the session
        const { startedAtTick } = await this.fetch(`sync/${sessionId}/metadata`);
        return {
            sessionId,
            startedAtTick
        };
    }
    async markSessionErrored(sessionId, error) {
        return this.fetch(`sync/${sessionId}/error`, {
            method: 'POST',
            body: {
                error
            }
        });
    }
    async endSyncSession(sessionId) {
        return this.fetch(`sync/${sessionId}`, {
            method: 'DELETE'
        });
    }
    async initiatePull(sessionId, since) {
        // first, set the pull filter on the central server,
        // which will kick off a snapshot of changes to pull
        const facilityIds = (0, _selectFacilityIds.selectFacilityIds)(_config.default);
        const body = {
            since,
            facilityIds,
            deviceId: this.deviceId
        };
        await this.fetch(`sync/${sessionId}/pull/initiate`, {
            method: 'POST',
            body
        });
        // then, wait for the pull/ready endpoint until we get a valid response;
        // it takes a while for pull/initiate to finish populating the snapshot of changes
        if (await this.streaming()) {
            for await (const { kind, message } of this.stream(()=>({
                    endpoint: `sync/${sessionId}/pull/ready/stream`
                }))){
                handler: switch(kind){
                    case _constants.SYNC_STREAM_MESSAGE_KIND.PULL_WAITING:
                        break handler;
                    case _constants.SYNC_STREAM_MESSAGE_KIND.END:
                        // includes the metadata for the changes we're about to pull
                        return {
                            sessionId,
                            ...message
                        };
                    default:
                        _logging.log.warn(`Unexpected message kind: ${kind}`);
                }
            }
            throw new Error('Unexpected end of stream');
        }
        await this.pollUntilTrue(`sync/${sessionId}/pull/ready`);
        // when polling, we need to separately fetch the metadata for the changes we're about to pull
        return this.fetch(`sync/${sessionId}/pull/metadata`);
    }
    async pull(sessionId, { limit = 100, fromId } = {}) {
        const query = {
            limit
        };
        if (fromId) {
            query.fromId = fromId;
        }
        return this.fetch(`sync/${sessionId}/pull`, {
            query
        });
    }
    async push(sessionId, changes) {
        const path = `sync/${sessionId}/push`;
        return this.fetch(path, {
            method: 'POST',
            body: {
                changes
            }
        });
    }
    async completePush(sessionId) {
        // first off, mark the push as complete on central
        await this.fetch(`sync/${sessionId}/push/complete`, {
            method: 'POST',
            body: {
                deviceId: this.deviceId
            }
        });
        // now poll the complete check endpoint until we get a valid response - it takes a while for
        // the pushed changes to finish persisting to the central database
        await this.pollUntilTrue(`sync/${sessionId}/push/complete`);
    }
    async whoami() {
        return this.fetch('whoami');
    }
    async forwardRequest(req, endpoint) {
        return this.fetch(endpoint, {
            method: req.method,
            body: req.body
        });
    }
    constructor({ deviceId }){
        const url = new URL(_config.default.sync.host.trim());
        url.pathname = '/api';
        super({
            logger: _logging.log,
            endpoint: url.toString(),
            agentName: _constants.SERVER_TYPES.FACILITY,
            agentVersion: _serverInfo.version,
            deviceId,
            defaultRequestConfig: {
                timeout: _config.default.sync.timeout,
                waitForAuth: true,
                backoff: true
            }
        }), _class_private_field_init(this, _loginData, {
            writable: true,
            value: void 0
        });
    }
};

//# sourceMappingURL=CentralServerConnection.js.map