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