"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setErrorRemapping = exports.withTransport = exports.cancelDeviceAction = exports.Transport = void 0;
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const logs_1 = require("@ledgerhq/logs");
const hw_transport_1 = __importDefault(require("@ledgerhq/hw-transport"));
exports.Transport = hw_transport_1.default;
const errors_1 = require("@ledgerhq/errors");
const index_1 = require("../../hw/index");
const deviceAccess_1 = require("../../hw/deviceAccess");
const LOG_TYPE = "device-sdk-transport";
const identifyTransport = t => (typeof t.id === "string" ? t.id : "");
const needsCleanup = {};
// When a series of APDUs are interrupted, this is called
// so we don't forget to cleanup on the next withTransport
const cancelDeviceAction = (transport) => {
    const transportId = identifyTransport(transport);
    (0, logs_1.trace)({
        type: LOG_TYPE,
        message: "Cancelling device action",
        data: { transportId },
    });
    needsCleanup[identifyTransport(transport)] = true;
};
exports.cancelDeviceAction = cancelDeviceAction;
/**
 * Wrapper providing a transport that can be refreshed to a job that can be executed
 *
 * This is useful for any job that may need to refresh the transport because of a device disconnected-like error
 *
 * The transportRef.current will be updated with the new transport when transportRef.refreshTransport is called
 *
 * @param deviceId the id of the device to open the transport channel with
 * @param options contains optional configuration
 *   - openTimeoutMs: optional timeout that limits in time the open attempt of the matching registered transport.
 * @returns a function that takes a job and returns an observable on the result of the job
 *  job: the job to execute with the transport. It takes:
 *   - transportRef: a reference to the transport that can be updated by calling a transportRef.refreshTransport()
 */
const withTransport = (deviceId, options) => {
    return (job) => new rxjs_1.Observable(subscriber => {
        const queuedJobManager = deviceAccess_1.DeviceQueuedJobsManager.getInstance();
        const previousQueuedJob = queuedJobManager.getLastQueuedJob(deviceId);
        // When the new job is finished, this will unlock the associated device queue of jobs
        let resolveQueuedJob;
        const jobId = queuedJobManager.setLastQueuedJob(deviceId, new Promise(resolve => {
            resolveQueuedJob = resolve;
        }));
        const tracer = new logs_1.LocalTracer(LOG_TYPE, {
            jobId,
            deviceId,
            origin: "deviceSdk:withTransport",
        });
        tracer.trace(`New job for device: ${deviceId || "USB"}`);
        // To call to cleanup the current transport
        const finalize = (transport, cleanups) => {
            tracer.trace("Closing and cleaning transport");
            return (0, index_1.close)(transport, deviceId)
                .catch(() => { })
                .then(() => {
                cleanups.forEach(c => c());
            });
        };
        let unsubscribed;
        let sub;
        // Setups a given transport
        const setupTransport = async (transport) => {
            tracer.trace("Setting up the transport instance");
            if (unsubscribed) {
                tracer.trace("Unsubscribed (1) while processing job");
                // It was unsubscribed prematurely
                return finalize(transport, [resolveQueuedJob]);
            }
            if (needsCleanup[identifyTransport(transport)]) {
                delete needsCleanup[identifyTransport(transport)];
                await transport.send(0, 0, 0, 0).catch(() => { });
            }
            return transport;
        };
        // This catch is here only for errors that might happen at open or at clean up of the transport before doing the job
        const onErrorDuringTransportSetup = (error) => {
            tracer.trace("Error while setting up a transport: ", { error });
            resolveQueuedJob();
            if (error instanceof errors_1.BluetoothRequired)
                throw error;
            if (error instanceof errors_1.TransportWebUSBGestureRequired)
                throw error;
            if (error instanceof errors_1.TransportInterfaceNotAvailable)
                throw error;
            if (error instanceof errors_1.PeerRemovedPairing)
                throw error;
            if (error instanceof errors_1.PairingFailed)
                throw error;
            if (error instanceof Error)
                throw new errors_1.CantOpenDevice(error.message);
            else
                throw new errors_1.CantOpenDevice("Unknown error");
        };
        // Returns an object containing: the reference to a transport and a function to refresh it
        const buildRefreshableTransport = (transport) => {
            tracer.trace("Creating a refreshable transport from a given instance");
            const transportRef = {
                current: transport,
                _refreshedCounter: 0,
                refreshTransport: Promise.resolve,
            };
            tracer.updateContext({ transportRefreshedCounter: transportRef._refreshedCounter });
            transportRef.refreshTransport = async () => {
                tracer.trace("Refreshing current transport");
                return ((0, index_1.close)(transportRef.current, deviceId)
                    // Silently ignore errors on transport close
                    .catch(() => { })
                    .then(async () => (0, index_1.open)(deviceId, options, tracer.getContext()))
                    .then(async (newTransport) => {
                    await setupTransport(transportRef.current);
                    transportRef.current = newTransport;
                    transportRef._refreshedCounter++;
                    tracer.updateContext({ transportRefreshedCounter: transportRef._refreshedCounter });
                    tracer.trace("Transport was refreshed");
                })
                    .catch(onErrorDuringTransportSetup));
            };
            return transportRef;
        };
        tracer.trace("Waiting for the previous job in the queue to complete", {
            previousJobId: previousQueuedJob.id,
        });
        // For any new job, we'll now wait the exec queue to be available
        previousQueuedJob.job
            .then(() => {
            tracer.trace("Previous queued job resolved, now trying to get a Transport instance", {
                previousJobId: previousQueuedJob.id,
                currentJobId: jobId,
            });
            return (0, index_1.open)(deviceId, options, tracer.getContext());
        })
            .then(transport => {
            return buildRefreshableTransport(transport);
        })
            .then(async (transportRef) => {
            await setupTransport(transportRef.current);
            return transportRef;
        })
            .catch(onErrorDuringTransportSetup)
            // Executes the job
            .then(transportRef => {
            tracer.trace("Executing job", { hasTransport: !!transportRef, unsubscribed });
            if (!transportRef.current)
                return;
            if (unsubscribed) {
                tracer.trace("Unsubscribed (2) while processing job");
                // It was unsubscribed prematurely
                return finalize(transportRef.current, [resolveQueuedJob]);
            }
            sub = job({ transportRef })
                .pipe((0, operators_1.catchError)(error => initialErrorRemapping(error, tracer.getContext())), (0, operators_1.catchError)(errorRemapping), // close the transport and clean up everything
            transportFinally(() => {
                return finalize(transportRef.current, [resolveQueuedJob]);
            }))
                .subscribe(subscriber);
        })
            .catch(error => {
            subscriber.error(error);
        });
        // Returns function to unsubscribe from the job if we don't need it anymore.
        // This will prevent us from executing the job unnecessarily later on
        return () => {
            tracer.trace(`Unsubscribing withDevice flow. Ongoing job to unsubscribe from ? ${!!sub}`);
            unsubscribed = true;
            if (sub)
                sub.unsubscribe();
        };
    });
};
exports.withTransport = withTransport;
/**
 * Wrapper to pipe a "cleanup" function at then end of an Observable flow.
 *
 * The `finalize` is only called once if there is an error and a complete
 * (but normally an error event completes automatically the Observable pipes. Is it needed ?)
 */
const transportFinally = (cleanup) => (observable) => new rxjs_1.Observable(o => {
    let done = false;
    const finalize = () => {
        if (done)
            return Promise.resolve();
        done = true;
        return cleanup();
    };
    const sub = observable.subscribe({
        next: e => o.next(e),
        complete: () => {
            finalize().then(() => o.complete());
        },
        error: e => {
            finalize().then(() => o.error(e));
        },
    });
    return () => {
        sub.unsubscribe();
        finalize();
    };
});
const initialErrorRemapping = (error, context) => {
    let mappedError = error;
    if (error && error instanceof errors_1.TransportStatusError) {
        if (error.statusCode === 0x6faa) {
            mappedError = new errors_1.DeviceHalted(error.message);
        }
        else if (error.statusCode === 0x6b00) {
            mappedError = new errors_1.FirmwareOrAppUpdateRequired(error.message);
        }
    }
    (0, logs_1.trace)({
        type: LOG_TYPE,
        message: `Initial error remapping: ${error}`,
        data: { error, mappedError },
        context,
    });
    return (0, rxjs_1.throwError)(() => mappedError);
};
let errorRemapping = e => (0, rxjs_1.throwError)(() => e);
const setErrorRemapping = (f) => {
    errorRemapping = f;
};
exports.setErrorRemapping = setErrorRemapping;
//# sourceMappingURL=core.js.map