UNPKG

@ledgerhq/live-common

Version:
126 lines (114 loc) 4.4 kB
import Transport from "@ledgerhq/hw-transport"; import { Observable, concat, of, from, EMPTY, defer } from "rxjs"; import { ConnectAppEvent } from "../hw/connectApp"; import getDeviceInfo from "../hw/getDeviceInfo"; import { listAppsUseCase } from "../device/use-cases/listAppsUseCase"; import { execWithTransport } from "../device/use-cases/execWithTransport"; import { reducer, initState, isOutOfMemoryState, predictOptimisticState } from "./logic"; import { runAllWithProgress } from "./runner"; import { InlineAppInstallEvent } from "./types"; import { mergeMap, map, throttleTime } from "rxjs/operators"; import { LocalTracer } from "@ledgerhq/logs"; /** * Tries to install a list of apps * * @param transport Transport instance * @param appNames List of app names to install * @param onSuccessObs Optional observable to run after the installation * @param allowPartialDependencies If true, keep installing apps even if some are missing * @returns Observable of InlineAppInstallEvent or ConnectAppEvent * - Event "inline-install" contains a global progress of the installation */ const inlineAppInstall = ({ transport, appNames, onSuccessObs, allowPartialDependencies = false, }: { transport: Transport; appNames: string[]; onSuccessObs?: () => Observable<any>; allowPartialDependencies?: boolean; }): Observable<InlineAppInstallEvent | ConnectAppEvent> => { const tracer = new LocalTracer("hw", { ...transport.getTraceContext(), function: "inlineAppInstall", }); tracer.trace("Starting inline app install"); return concat( of({ type: "listing-apps", }), from(getDeviceInfo(transport)).pipe( mergeMap(deviceInfo => { tracer.trace("Got device info", { deviceInfo }); return listAppsUseCase(transport, deviceInfo); }), mergeMap(e => { // Bubble up events if (e.type === "device-permission-granted" || e.type === "device-permission-requested") { return of(e); } if (e.type === "result") { // Figure out the operations that need to happen const state = appNames.reduce( (state, name) => reducer(state, { type: "install", name, allowPartialDependencies, }), initState(e.result), ); // Failed appOps in this flow will throw by default but if we're here // it means we didn't throw, so we wan't to notify the action about it. const maybeSkippedEvent: Observable<InlineAppInstallEvent> = state.skippedAppOps.length ? of({ type: "some-apps-skipped", skippedAppOps: state.skippedAppOps, }) : EMPTY; if (!state.installQueue.length) { // There's nothing to install, we can consider this a success. return defer(onSuccessObs || (() => EMPTY)); } if (isOutOfMemoryState(predictOptimisticState(state))) { // In this case we can't install either by lack of storage, or permissions, // we fallback to the error case listing the missing apps. const missingAppNames: string[] = state.installQueue; tracer.trace("Out of memory", { appNames: missingAppNames, appName: missingAppNames[0] || appNames[0], }); return of({ type: "app-not-installed", appNames: missingAppNames, appName: missingAppNames[0] || appNames[0], // TODO remove when LLD/LLM integrate appNames }); } const exec = execWithTransport(transport); return concat( of({ type: "listed-apps", installQueue: state.installQueue, }), maybeSkippedEvent, runAllWithProgress(state, exec).pipe( throttleTime(100), map(({ globalProgress, itemProgress, installQueue, currentAppOp }) => ({ type: "inline-install", progress: globalProgress, itemProgress, installQueue, currentAppOp, })), ), defer(onSuccessObs || (() => EMPTY)), ); } return EMPTY; }), ), ); }; export default inlineAppInstall;