Farmbot-Web-App/frontend/connectivity/data_consistency.ts

98 lines
3.5 KiB
TypeScript

import { getDevice } from "../device";
import { store } from "../redux/store";
import { Actions } from "../constants";
import { set } from "lodash";
interface NonSense {
last: string;
all: Set<string>;
}
export const outstandingRequests: NonSense = {
last: "never-used",
all: new Set()
};
export function storeUUID(uuid: string) {
outstandingRequests.last = cleanUUID(uuid);
outstandingRequests.all.add(outstandingRequests.last);
}
function unstoreUUID(uuid: string) {
outstandingRequests.all.delete(PLACEHOLDER);
outstandingRequests.all.delete(cleanUUID(uuid));
}
set(window, "outstanding_requests", outstandingRequests);
/** Use this when you need to throw the FE into an inconsistent state, but dont
* have a real UUID available. It will be removed when a "real" UUID comes
* along. This is necessary for creating an instantaneous "syncing..." label. */
const PLACEHOLDER = "placeholder";
/** Max wait in MS before clearing out. */
const MAX_WAIT = 11000;
/**
* PROBLEM: You save a sequence and click "RUN" very fast. The remote device
* did not have time to download the new sequence and so it crashes
* with a "sequence not found error". This is a result of an
* inconsistency between the local FE and FBOS.
*
* SOLUTION:
*
* - On all AJAX requests, the API attaches an `X-Farmbot-Rpc-Id` header (UUID).
* - On all auto_sync messages, the API attaches the same UUID under
* msg.args.label
* - When FBOS gets an auto_sync message, it replies with an `rpc_ok` message
* that has the same `label` as the API request.
* - We keep a list of `outstandingRequests` filled with such UUIDs.
* - When we get an `rpc_ok` from farmbot, we remove it from the list.
* - If the request takes longer than 5 seconds, remove it from the list also
* to prevent accidental UX issues. ("forceful removal")
* - When `outstandingRequests.size === 0`, you can (probably) assume that the
* Bot, API and client are in a consistent state. It is safe to perform data
* intensive operations.
*
* LONG TERM SOLUTION: TODO:
*
* - We should consider moving CRUD operations into FarmBotJS and provide
* developers a unified API that handles these things.
* - If data operations were RPCs instead of REST calls, we would not need
* this and we could track data operations the same way a `exec_sequence`
* and friends.
*/
export function startTracking(uuid = PLACEHOLDER) {
const cleanID = cleanUUID(uuid);
ifQueueEmpty(() => store.dispatch(stash()));
const isConsistent = getConsistencyState();
if (isConsistent) {
store.dispatch(setConsistency(false));
}
storeUUID(cleanID);
getDevice().on(cleanID, () => stopTracking(cleanID));
setTimeout(() => stopTracking(uuid), MAX_WAIT);
}
export function stopTracking(uuid: string) {
const cleanID = cleanUUID(uuid);
unstoreUUID(cleanID);
// Purpose: Determine if dispatch is actually required to avoid dispatching
// too many times for the same value.
if (!getConsistencyState()) {
ifQueueEmpty(() => store.dispatch(setConsistency(true)));
}
}
const setConsistency =
(payload: boolean) => ({ type: Actions.SET_CONSISTENCY, payload });
export const stash =
() => ({ type: Actions.STASH_STATUS, payload: undefined });
const ifQueueEmpty =
<T>(cb: () => T): T | false => (outstandingRequests.all.size === 0) && cb();
const getConsistencyState = () => !!store.getState().bot.consistent;
/** HTTP servers were stripping dots out of our UUIDs in headers...? */
export const cleanUUID =
(uuid: string) => uuid.toLowerCase().split(".").join("");