UNPKG

@ledgerhq/live-common

Version:
180 lines (168 loc) 4.41 kB
import { Observable, from, of, defer, concat } from "rxjs"; import { map, materialize, reduce, ignoreElements, throttleTime, scan, mergeMap, distinctUntilChanged, } from "rxjs/operators"; import type { Exec, State, AppOp, RunnerEvent, Action } from "./types"; import { reducer, getActionPlan, getNextAppOp } from "./logic"; import { delay } from "../promise"; import { getEnv } from "@ledgerhq/live-env"; export const runAppOp = ({ state, appOp, exec, }: { state: State; appOp: AppOp; exec: Exec; }): Observable<RunnerEvent> => { const { appByName, deviceInfo, deviceModel } = state; const app = appByName[appOp.name]; if (!app) { const events: RunnerEvent[] = [ { type: "runStart", appOp, }, { type: "runSuccess", appOp, }, ]; // app not in list, we skip it. return from(events); } return concat( of(<RunnerEvent>{ type: "runStart", appOp, }), // we need to allow a 1s delay for the action to be achieved without glitch (bug in old firmware when you do things too closely) defer(() => delay(getEnv("MANAGER_INSTALL_DELAY"))).pipe(ignoreElements()), defer(() => exec({ appOp, targetId: deviceInfo.targetId, app, modelId: deviceModel.id, ...(state.skipAppDataBackup ? { skipAppDataBackup: true } : {}), }), ).pipe( throttleTime(100), materialize(), map(n => { switch (n.kind) { case "N": return <RunnerEvent>{ type: "runProgress", appOp, progress: n?.value?.progress ?? 0, }; case "E": return <RunnerEvent>{ type: "runError", appOp, error: n.error, }; case "C": return <RunnerEvent>{ type: "runSuccess", appOp, }; } }), ), ); }; type InlineInstallProgress = { globalProgress: number; itemProgress: number; currentAppOp: AppOp | null | undefined; installQueue: string[]; }; export const runAllWithProgress = ( state: State, exec: Exec, precision = 100, ): Observable<InlineInstallProgress> => { const total = state.uninstallQueue.length + state.installQueue.length; function globalProgress(s, localProgress) { let p = 1 - (s.uninstallQueue.length + s.installQueue.length - localProgress) / total; p = Math.round(p * precision) / precision; return p; } return concat(...getActionPlan(state).map(appOp => runAppOp({ state, appOp, exec }))).pipe( map(event => { if (event.type === "runError") { throw event.error; } return <Action>{ type: "onRunnerEvent", event, }; }), scan(reducer, state), mergeMap(s => { // Nb if you also want to expose the uninstall queue, feel free. const { currentProgressSubject, currentAppOp, installQueue } = s; if (!currentProgressSubject) return of({ globalProgress: globalProgress(s, 0), itemProgress: 0, installQueue, currentAppOp, }); // Expose more information about what's happening during the install return currentProgressSubject.pipe( map(v => ({ globalProgress: globalProgress(s, v), itemProgress: v, installQueue, currentAppOp, })), ); }), distinctUntilChanged(), ); }; // use for CLI, no change of the state over time export const runAll = (state: State, exec: Exec): Observable<State> => concat(...getActionPlan(state).map(appOp => runAppOp({ state, appOp, exec }))).pipe( map( event => <Action>{ type: "onRunnerEvent", event, }, ), reduce(reducer, state), ); export const runOneAppOp = ({ state, appOp, exec, }: { state: State; appOp: AppOp; exec: Exec; }): Observable<State> => runAppOp({ state, appOp, exec }).pipe( map( event => <Action>{ type: "onRunnerEvent", event, }, ), reduce(reducer, state), ); export const runOne = ({ state, exec }: { state: State; exec: Exec }): Observable<State> => { const next = getNextAppOp(state); if (!next) return of(state); return runOneAppOp({ state, appOp: next, exec }); };