wallet-core/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts

299 lines
7.9 KiB
TypeScript
Raw Normal View History

2021-10-14 17:08:41 +02:00
import { TalerErrorCode } from "@gnu-taler/taler-util";
2021-10-18 19:19:20 +02:00
import { BackupStates, getBackupStartState, getRecoveryStartState, RecoveryStates, reduceAction, ReducerState } from "anastasis-core";
2021-10-11 10:58:55 +02:00
import { useState } from "preact/hooks";
2021-10-18 19:19:20 +02:00
const reducerBaseUrl = "http://localhost:5000/";
2021-10-19 15:56:52 +02:00
const remoteReducer = true;
2021-10-11 10:58:55 +02:00
interface AnastasisState {
reducerState: ReducerState | undefined;
currentError: any;
}
2021-10-18 19:19:20 +02:00
async function getBackupStartStateRemote(): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("start-backup", reducerBaseUrl).href);
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-18 19:19:20 +02:00
async function getRecoveryStartStateRemote(): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("start-recovery", reducerBaseUrl).href);
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-18 19:19:20 +02:00
async function reduceStateRemote(
2021-10-11 10:58:55 +02:00
state: any,
action: string,
args: any,
): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("action", reducerBaseUrl).href, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
state,
action,
arguments: args,
}),
});
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-13 10:48:25 +02:00
export interface ReducerTransactionHandle {
transactionState: ReducerState;
transition(action: string, args: any): Promise<ReducerState>;
}
2021-10-11 10:58:55 +02:00
export interface AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
currentReducerState: ReducerState | undefined;
2021-10-11 10:58:55 +02:00
currentError: any;
2021-10-13 10:48:25 +02:00
dismissError: () => void;
2021-10-11 10:58:55 +02:00
startBackup: () => void;
startRecover: () => void;
2021-10-13 10:48:25 +02:00
reset: () => void;
2021-10-11 10:58:55 +02:00
back: () => void;
transition(action: string, args: any): void;
2021-10-13 10:48:25 +02:00
/**
* Run multiple reducer steps in a transaction without
* affecting the UI-visible transition state in-between.
*/
runTransaction(f: (h: ReducerTransactionHandle) => Promise<void>): void;
}
function storageGet(key: string): string | null {
if (typeof localStorage === "object") {
return localStorage.getItem(key);
}
return null;
}
function storageSet(key: string, value: any): void {
if (typeof localStorage === "object") {
return localStorage.setItem(key, value);
}
}
2021-10-13 10:48:25 +02:00
function restoreState(): any {
let state: any;
try {
2021-10-19 15:56:52 +02:00
const s = storageGet("anastasisReducerState");
2021-10-13 10:48:25 +02:00
if (s === "undefined") {
state = undefined;
} else if (s) {
console.log("restoring state from", s);
state = JSON.parse(s);
}
} catch (e) {
console.log(e);
}
return state ?? undefined;
2021-10-11 10:58:55 +02:00
}
export function useAnastasisReducer(): AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>(
() => ({
reducerState: restoreState(),
currentError: undefined,
}),
);
const setAnastasisState = (newState: AnastasisState) => {
try {
storageSet(
2021-10-13 10:48:25 +02:00
"anastasisReducerState",
JSON.stringify(newState.reducerState),
);
} catch (e) {
console.log(e);
}
setAnastasisStateInternal(newState);
};
2021-10-11 10:58:55 +02:00
async function doTransition(action: string, args: any) {
console.log("reducing with", action, args);
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await reduceStateRemote(anastasisState.reducerState, action, args);
} else {
s = await reduceAction(anastasisState.reducerState!, action, args);
}
2021-10-11 10:58:55 +02:00
console.log("got new state from reducer", s);
if (s.code) {
setAnastasisState({ ...anastasisState, currentError: s });
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
}
return {
currentReducerState: anastasisState.reducerState,
currentError: anastasisState.currentError,
async startBackup() {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await getBackupStartStateRemote();
} else {
s = await getBackupStartState();
}
2021-10-14 17:08:41 +02:00
if (s.code !== undefined) {
setAnastasisState({
...anastasisState,
currentError: s,
});
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
2021-10-11 10:58:55 +02:00
},
async startRecover() {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await getRecoveryStartStateRemote();
} else {
s = await getRecoveryStartState();
}
2021-10-14 17:08:41 +02:00
if (s.code !== undefined) {
setAnastasisState({
...anastasisState,
currentError: s,
});
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
2021-10-11 10:58:55 +02:00
},
transition(action: string, args: any) {
doTransition(action, args);
},
back() {
2021-10-13 10:48:25 +02:00
const reducerState = anastasisState.reducerState;
if (!reducerState) {
return;
}
2021-10-11 10:58:55 +02:00
if (
2021-10-13 10:48:25 +02:00
reducerState.backup_state === BackupStates.ContinentSelecting ||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
2021-10-11 10:58:55 +02:00
) {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: undefined,
});
} else {
doTransition("back", {});
}
},
2021-10-13 10:48:25 +02:00
dismissError() {
setAnastasisState({ ...anastasisState, currentError: undefined });
},
reset() {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: undefined,
});
},
runTransaction(f) {
async function run() {
const txHandle = new ReducerTxImpl(anastasisState.reducerState!);
try {
await f(txHandle);
} catch (e) {
console.log("exception during reducer transaction", e);
}
const s = txHandle.transactionState;
console.log("transaction finished, new state", s);
if (s.code !== undefined) {
setAnastasisState({
...anastasisState,
currentError: txHandle.transactionState,
});
} else {
setAnastasisState({
...anastasisState,
reducerState: txHandle.transactionState,
currentError: undefined,
});
}
}
run();
},
2021-10-11 10:58:55 +02:00
};
}
2021-10-13 10:48:25 +02:00
class ReducerTxImpl implements ReducerTransactionHandle {
constructor(public transactionState: ReducerState) {}
async transition(action: string, args: any): Promise<ReducerState> {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await reduceStateRemote(this.transactionState, action, args);
} else {
s = await reduceAction(this.transactionState, action, args);
}
2021-10-13 10:48:25 +02:00
console.log("making transition in transaction", action);
2021-10-18 19:19:20 +02:00
this.transactionState = s;
2021-10-13 10:48:25 +02:00
// Abort transaction as soon as we transition into an error state.
if (this.transactionState.code !== undefined) {
throw Error("transition resulted in error");
}
return this.transactionState;
}
}