@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
126 lines • 6.34 kB
JavaScript
;
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