UNPKG

@ledgerhq/live-common

Version:
126 lines 6.34 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isDmkError = exports.LOG_TYPE = exports.RETRY_ON_ERROR_DELAY_MS = exports.NO_RESPONSE_TIMEOUT_MS = void 0; exports.sharedLogicTaskWrapper = sharedLogicTaskWrapper; exports.retryOnErrorsCommandWrapper = retryOnErrorsCommandWrapper; const errors_1 = require("@ledgerhq/errors"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); exports.NO_RESPONSE_TIMEOUT_MS = 30000; exports.RETRY_ON_ERROR_DELAY_MS = 500; exports.LOG_TYPE = "device-sdk-task"; /** * Wraps a task function to add some common logic to it: * - Timeout for no response * - Retry strategy on specific errors: those errors are fixed for all tasks * - Catch errors and emit them as events * * @param task The task function to wrap * @returns A wrapped task function */ function sharedLogicTaskWrapper(task, defaultTimeoutOverride) { return (args) => { return new rxjs_1.Observable(subscriber => { return task(args) .pipe((0, operators_1.timeout)(defaultTimeoutOverride ?? exports.NO_RESPONSE_TIMEOUT_MS), (0, operators_1.retry)({ delay: error => { let acceptedError = false; // - LockedDeviceError and UnresponsiveDeviceError: on every transport if there is a device but it is locked // - CantOpenDevice: it can come from hw-transport-node-hid-singleton/TransportNodeHid // or react-native-hw-transport-ble/BleTransport when no device is found // - DisconnectedDevice: it can come from TransportNodeHid while switching app if (error instanceof errors_1.LockedDeviceError || error instanceof errors_1.UnresponsiveDeviceError || error instanceof errors_1.CantOpenDevice || error instanceof errors_1.DisconnectedDevice || error instanceof errors_1.TransportRaceCondition) { // Emits to the action an error event so it is aware of it (for ex locked device) before retrying const event = { type: "error", error, retrying: true, }; subscriber.next(event); acceptedError = true; } return acceptedError ? (0, rxjs_1.timer)(exports.RETRY_ON_ERROR_DELAY_MS) : (0, rxjs_1.throwError)(() => error); }, }), (0, operators_1.catchError)((error) => { // Emits the error to the action, without throwing return (0, rxjs_1.of)({ type: "error", error, retrying: false }); })) .subscribe(subscriber); }); }; } const isDmkError = (error) => !!error && typeof error === "object" && error !== null && "_tag" in error; exports.isDmkError = isDmkError; /** * Calls a command and retries it on given errors. The transport is refreshed before each retry. * * The no response timeout is handled at the task level * * @param command the command to wrap, it should take as argument an object containing a transport * @param transportRef a reference to the transport, that can be updated/refreshed * @param allowedErrors a list of errors to retry on * - errorClass: the error class to retry on * - maxRetries: the maximum number of retries for this error */ function retryOnErrorsCommandWrapper({ command, allowedErrors, allowedDmkErrors = [], }) { // Returns the command wrapped into the retry mechanism // No need to pass the transport to the wrapped command return (transportRef, argsWithoutTransport) => { let sameErrorInARowCount = 0; let shouldRefreshTransport = false; let latestErrorName = null; // It cannot start with the command itself because of the retry and the transport reference: // the retry will be chained on the observable returned before the pipe and if it is the command itself, // it would retry the command with the same transport and not the refreshed one return (0, rxjs_1.of)(1).pipe((0, operators_1.switchMap)(() => { if (shouldRefreshTransport) { // if we pass through this code again it means that some error happened during the command // execution, therefore we'll then, and only then, start refreshing the transport // before trying the command again return (0, rxjs_1.from)(transportRef.refreshTransport()); } shouldRefreshTransport = true; return (0, rxjs_1.of)(1); }), // Overrides the transport instance so it can be refreshed (0, operators_1.concatMap)(() => { return command({ ...argsWithoutTransport, transport: transportRef.current, }); }), (0, operators_1.retry)({ delay: error => { let isAllowedError = false; if (latestErrorName !== error.name) { sameErrorInARowCount = 0; } else { sameErrorInARowCount++; } latestErrorName = error.name; for (const { errorClass, maxRetries } of allowedErrors) { if (error instanceof errorClass) { if (maxRetries === "infinite" || sameErrorInARowCount < maxRetries) { isAllowedError = true; } break; } } if ((0, exports.isDmkError)(error)) { isAllowedError = allowedDmkErrors.some(dmkError => dmkError._tag === error._tag); } if (isAllowedError) { // Retries the whole pipe chain after the delay return (0, rxjs_1.timer)(exports.RETRY_ON_ERROR_DELAY_MS); } // If the error is not part of the allowed errors, it is thrown return (0, rxjs_1.throwError)(() => error); }, })); }; } //# sourceMappingURL=core.js.map