@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
126 lines (114 loc) • 4.4 kB
text/typescript
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;