UNPKG

@ledgerhq/live-common

Version:
201 lines • 8.84 kB
"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