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