"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getImplementation = exports.eventImplementation = exports.pollingImplementation = exports.defaultImplementationConfig = exports.ImplementationType = void 0;
const rxjs_1 = require("rxjs");
const errors_1 = require("@ledgerhq/errors");
const operators_1 = require("rxjs/operators");
const devices_1 = require("@ledgerhq/devices");
const errors_2 = require("../../errors");
const logs_1 = require("@ledgerhq/logs");
const __1 = require("..");
const live_env_1 = require("@ledgerhq/live-env");
var ImplementationType;
(function (ImplementationType) {
    ImplementationType["event"] = "enum";
    ImplementationType["polling"] = "polling";
})(ImplementationType || (exports.ImplementationType = ImplementationType = {}));
const defaultRetryableWithDelayDisconnectedErrors = [
    errors_1.DisconnectedDevice,
    errors_1.DisconnectedDeviceDuringOperation,
];
exports.defaultImplementationConfig = {
    pollingFrequency: 2000,
    initialWaitTime: 5000,
    reconnectWaitTime: 5000,
    connectionTimeout: (0, live_env_1.getEnv)("DETOX") ? 60000 : 20000,
};
const remapError = (error, deviceModelID) => {
    if (!(error instanceof rxjs_1.TimeoutError)) {
        return error;
    }
    if (deviceModelID === devices_1.DeviceModelId.nanoS) {
        return new errors_1.LockedDeviceError();
    }
    return new errors_2.ConnectManagerTimeout(undefined, {
        // TODO make this configurable
        productName: (0, devices_1.getDeviceModel)(deviceModelID).productName,
    });
};
/**
 * Returns a polling implementation function that repeatedly performs a given task
 * with a given request, with a specified polling frequency and wait times until a
 * KO or OK end state is reached. This emulates the event based connection paradigm
 * from the USB stack in a BLE setting.
 * @template EmittedEvent - Type of the events emitted by the task's observable.
 * @template GenericRequestType - Type of the request to be passed to the task.
 **/
const pollingImplementation = (params) => new rxjs_1.Observable((subscriber) => {
    const { deviceSubject, task, request, config = exports.defaultImplementationConfig } = params;
    const { pollingFrequency, initialWaitTime, reconnectWaitTime, connectionTimeout } = config;
    const tracer = new logs_1.LocalTracer(__1.LOG_TYPE, { function: "pollingImplementation" });
    tracer.trace("Setting up device action polling implementation", { config });
    let shouldStopPolling = false;
    let connectSub;
    let loopTimeout;
    let firstRound = true;
    let currentDevice;
    const deviceSubjectSub = deviceSubject.subscribe(device => {
        currentDevice = device || currentDevice;
    });
    // Cover case where our device job never gets a transport instance
    // it will signal back to the consumer that we don't have a device.
    let initialTimeout = setTimeout(() => {
        return subscriber.next({
            type: "deviceChange",
            device: undefined,
        });
    }, initialWaitTime);
    // Runs every time we get a new device.
    function actionLoop() {
        if (currentDevice) {
            connectSub = (0, rxjs_1.concat)((0, rxjs_1.of)({
                type: "deviceChange",
                device: currentDevice,
                replaceable: !firstRound,
            }), task({
                deviceId: currentDevice.deviceId,
                deviceName: currentDevice.deviceName ?? null,
                request,
            }))
                .pipe(
            // Any event should clear the initialTimeout.
            (0, operators_1.tap)((_) => {
                if (initialTimeout) {
                    clearTimeout(initialTimeout);
                    initialTimeout = null;
                }
            }), 
            // More than `connectionTimeout` time of silence, is considered
            // a failed connection. Depending on the flow this may mean we'd have
            // to emit an `alive` kind of event.
            (0, operators_1.timeout)(connectionTimeout), (0, operators_1.filter)((event) => {
                if ("type" in event) {
                    return event.type !== "unresponsiveDevice";
                }
                return false;
            }), (0, operators_1.catchError)((error) => {
                const maybeRemappedError = remapError(error, currentDevice.modelId);
                tracer.trace(`Error when running task in polling implementation: ${error}`, {
                    error,
                    maybeRemappedError,
                });
                return (0, rxjs_1.of)({
                    type: "error",
                    error: maybeRemappedError,
                });
            }), (0, operators_1.debounce)((event) => {
                if (event.type === "error" && "error" in event) {
                    const allowedRetryableErrors = params.retryableWithDelayDisconnectedErrors ||
                        defaultRetryableWithDelayDisconnectedErrors;
                    const error = event.error;
                    if (allowedRetryableErrors.find(e => error instanceof e)) {
                        // Delay emission of allowed disconnects to allow reconnection.
                        return (0, rxjs_1.timer)(reconnectWaitTime);
                    }
                    // Other errors should cancel the loop.
                    shouldStopPolling = true;
                }
                // All other events pass through.
                return (0, rxjs_1.of)(null);
            }))
                .subscribe({
                next: (event) => {
                    subscriber.next(event);
                },
                error: (e) => {
                    subscriber.error(e);
                },
                complete: () => {
                    if (shouldStopPolling)
                        return;
                    firstRound = false; // For proper debouncing.
                    loopTimeout = setTimeout(actionLoop, pollingFrequency);
                },
            });
        }
        else {
            // We currently don't have a device, try again.
            loopTimeout = setTimeout(actionLoop, pollingFrequency);
        }
    }
    // Delay the first loop run in order to be async.
    loopTimeout = setTimeout(actionLoop, 0);
    // Get rid of pending timeouts and subscriptions.
    return function cleanup() {
        if (deviceSubjectSub)
            deviceSubjectSub.unsubscribe();
        if (connectSub)
            connectSub.unsubscribe();
        if (initialTimeout)
            clearTimeout(initialTimeout);
        if (loopTimeout)
            clearTimeout(loopTimeout);
    };
});
exports.pollingImplementation = pollingImplementation;
const eventImplementation = (params) => {
    const { deviceSubject, task, request, config = exports.defaultImplementationConfig } = params;
    const { reconnectWaitTime } = config;
    return new rxjs_1.Observable((subscriber) => {
        const connectSub = deviceSubject
            .pipe((0, operators_1.debounce)(device => (0, rxjs_1.timer)(!device ? reconnectWaitTime : 0)), (0, operators_1.switchMap)(device => {
            const initialEvent = (0, rxjs_1.of)({
                type: "deviceChange",
                device,
            });
            return (0, rxjs_1.concat)(initialEvent, device
                ? task({
                    deviceId: device.deviceId,
                    deviceName: device.deviceName ?? null,
                    request,
                })
                : rxjs_1.EMPTY);
        }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)({
            type: "error",
            error,
        })), 
        // NB An error is a dead-end as far as the task is concerned, and by delaying
        // the emission of the event we prevent instant failures from showing flashing
        // UI that looks like a glitch. For instance, if the device is locked and we retry
        // this would allow a better UX, 800ms before a failure is totally acceptable.
        (0, operators_1.delayWhen)((e) => e.type === "error" || e.type === "lockedDevice" ? (0, rxjs_1.interval)(800) : (0, rxjs_1.interval)(0)))
            .subscribe({
            next: (event) => {
                subscriber.next(event);
            },
            error: (e) => {
                subscriber.error(e);
            },
            complete: () => { },
        });
        // Get rid of subscriptions.
        return function cleanup() {
            if (connectSub)
                connectSub.unsubscribe();
        };
    });
};
exports.eventImplementation = eventImplementation;
const getImplementation = (currentMode) => currentMode === "polling" ? pollingImplementation : eventImplementation;
exports.getImplementation = getImplementation;
//# sourceMappingURL=implementations.js.map