cordis/client/webapp/auto-verifier.ts
2021-11-30 20:45:06 -06:00

263 lines
10 KiB
TypeScript

// import * as electronRemote from '@electron/remote';
// const electronConsole = electronRemote.getGlobal('console') as Console;
// import Logger from '../../logger/logger';
// const LOG = Logger.create(__filename, electronConsole);
import { Changes, WithEquals } from './data-types';
export enum AutoVerifierChangesType {
NONE, // Both primaryFunc and trustedFunc returned null
PRIMARY_ONLY, // primaryFunc returned non-null and trustedFunc returned null
TRUSTED_ONLY, // trustedFunc returned non-null and primaryFunc returned null
VERIFIED, // primaryFunc and trustedFunc returned the same non-null result
CONFLICT, // primaryFunc and trustedFunc returned conflicting non-null results
};
/**
* This is probably complex piece of code in this entire project.
* Some complexity comes because the result of the trusted function
* can be invalidated while we are awaiting the trusted function.
* That complexity stacks on top of the already complex cache-verification
* to make this one fiesta of a class.
* If you have to edit this it's a very sad day.
*/
export class AutoVerifier<T> {
public primaryPromise: Promise<T | null> | null = null;
public trustedPromise: Promise<T | null> | null = null;
public trustedStatus: 'none' | 'fetching' | 'verifying' | 'verified' | 'unverified' = 'none';
/**
* Allows a trusted function to verify the primary function
* @param primaryFunc The primary function
* @param trustedFunc The trusted function that will verify the results of the primary function (one time)
* @param verifyFunc The verification function that is called when the primary function's results need to be verified against the trusted function's results
*/
constructor(
private primaryFunc: () => Promise<T | null>,
private trustedFunc: () => Promise<T | null>,
private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise<void>
) {}
static getChanges<T extends WithEquals<T> & { id: string }>(primaryResult: T[] | null, trustedResult: T[] | null): Changes<T> {
let changes: Changes<T> = { added: [], updated: [], deleted: [] };
if (primaryResult === null && trustedResult === null) {
return changes;
} else if (primaryResult === null) {
changes.added = trustedResult as T[];
return changes;
} else if (trustedResult === null) {
changes.deleted = primaryResult as T[];
return changes;
}
primaryResult = primaryResult as T[];
trustedResult = trustedResult as T[];
for (let trustedElement of trustedResult) {
let primaryElement = primaryResult.find(primaryElement => primaryElement.id === trustedElement.id);
if (primaryElement) {
changes.updated.push({ oldDataPoint: primaryElement, newDataPoint: trustedElement });
} else {
changes.added.push(trustedElement);
}
}
for (let primaryElement of primaryResult) {
let trustedElement = trustedResult.find(trustedElement => trustedElement.id === primaryElement.id);
if (!trustedElement) {
changes.deleted.push(primaryElement);
}
}
return changes;
}
static getListChangesType<T>(primaryResult: T[] | null, trustedResult: T[] | null, changes: Changes<T>): AutoVerifierChangesType {
if (primaryResult === null && trustedResult === null) {
return AutoVerifierChangesType.NONE;
} else if (trustedResult === null) {
return AutoVerifierChangesType.PRIMARY_ONLY;
} else if (primaryResult === null) {
return AutoVerifierChangesType.TRUSTED_ONLY;
} else if (changes.added.length === 0 && changes.updated.length === 0 && changes.deleted.length === 0) {
return AutoVerifierChangesType.VERIFIED;
} else {
return AutoVerifierChangesType.CONFLICT;
}
}
static getSingleChangesType<T extends WithEquals<T>>(primaryResult: T | null, trustedResult: T | null): AutoVerifierChangesType {
if (primaryResult === null && trustedResult === null) {
return AutoVerifierChangesType.NONE;
} else if (trustedResult === null) {
return AutoVerifierChangesType.PRIMARY_ONLY;
} else if (primaryResult === null) {
return AutoVerifierChangesType.TRUSTED_ONLY;
} else if (primaryResult.equals(trustedResult)) {
return AutoVerifierChangesType.VERIFIED;
} else {
return AutoVerifierChangesType.CONFLICT;
}
}
static createStandardListAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: () => Promise<T[] | null>,
trustedFunc: () => Promise<T[] | null>,
changesFunc: (changesType: AutoVerifierChangesType, changes: Changes<T>) => Promise<void>
) {
return new AutoVerifier<T[]>(
primaryFunc,
trustedFunc,
async (primaryResult: T[] | null, trustedResult: T[] | null) => {
let changes = AutoVerifier.getChanges<T>(primaryResult, trustedResult);
let changesType = AutoVerifier.getListChangesType<T>(primaryResult, trustedResult, changes);
await changesFunc(changesType, changes);
}
);
}
static createStandardSingleAutoVerifier<T extends WithEquals<T>>(
primaryFunc: () => Promise<T | null>,
trustedFunc: () => Promise<T | null>,
changesFunc: (changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise<void>
) {
return new AutoVerifier<T>(
primaryFunc,
trustedFunc,
async (primaryResult: T | null, trustedResult: T | null) => {
let changesType = AutoVerifier.getSingleChangesType<T>(primaryResult, trustedResult);
await changesFunc(changesType, primaryResult, trustedResult);
}
);
}
// You CAN safely call this while another fetch is going on! How convenient
unverify(): void {
if (this.primaryPromise) {
this.primaryPromise.catch((e) => { console.warn('caught unverified promise', e); });
}
if (this.trustedPromise) {
this.trustedPromise.catch((e) => { console.warn('caught unverified promise', e); });
}
this.primaryPromise = null;
this.trustedPromise = null;
this.trustedStatus = 'none';
}
// Fetches the result of the primary fetchable
// If the primary fetchable returns null but has not been verified yet, this will return the result of the trusted fetchable
// If the trusted fetchable has not been used to verify the primary fetchable yet, this queries the trusted fetchable and calls verify
async fetchAndVerifyIfNeeded(): Promise<T | null> {
return await new Promise<T | null>(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
let resolved = false;
try {
let origTrustedStatus = this.trustedStatus;
let origTrustedPromise = this.trustedPromise;
if (this.primaryPromise === null) {
this.primaryPromise = this.primaryFunc();
}
let origPrimaryPromise = this.primaryPromise;
// pre-fetch the trusted result while we fetch the primary result
if (this.trustedStatus === 'none') {
this.trustedStatus = 'fetching';
this.trustedPromise = this.trustedFunc();
}
let primaryResult = await this.primaryPromise;
if (this.primaryPromise === origPrimaryPromise) {
this.primaryPromise = null;
}
if (primaryResult) {
resolve(primaryResult);
resolved = true;
}
//@ts-ignore (could be changed by an unverify during primaryPromise)
if (this.trustedStatus === 'none') {
// try to re-fetch the trusted result
this.trustedStatus = 'fetching';
this.trustedPromise = this.trustedFunc();
}
let tryResolveTrustedPromise = async () => {
if (this.trustedPromise) {
// There is a trusted promise that we can check
if (this.trustedStatus === 'fetching') {
// No one has started verifying the trusted yet
this.trustedStatus = 'verifying';
// Note: Promises that have already resolved will return the same value when awaited again :)
let origTrustedPromise: Promise<T | null> | null = this.trustedPromise;
let trustedResult = await origTrustedPromise;
if (this.trustedPromise !== origTrustedPromise) {
// we've been invalidated while we were waiting for the trusted result!
console.warn('RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!');
await tryResolveTrustedPromise();
return;
}
// Make sure to verify BEFORE potentially resolving
// This way the conflicts can be resolved before the result is returned
await this.verifyFunc(primaryResult, trustedResult);
if (this.trustedPromise === origTrustedPromise) {
this.trustedStatus = 'verified';
} else {
// this actually could be quite common
//console.warn('RARE ALERT: we got unverified during verification!');
// ** complexity:
// keep in mind that this will resolve 'unverified' promises with their resulting origTrustedPromise (which should be a good thing)
}
if (!resolved && trustedResult) {
resolve(trustedResult);
resolved = true;
}
} else {
// Some code is already dealing with (or dealt with) verifying the trusted result
// Await the same trusted promise and return its result if we didn't get a result
// from the primary source.
// Note: Promises that have already resolved will return the same value when awaited again :)
let origTrustedPromise: Promise<T | null> | null = this.trustedPromise;
let trustedResult = await origTrustedPromise;
if (this.trustedPromise !== origTrustedPromise) {
// we've been invalidated while we were waiting for the trusted result!
console.warn('ULTRA RARE ALERT: we got unverified while awaiting a trusted promise another path was verifying!');
await tryResolveTrustedPromise();
return;
}
if (!resolved) {
resolve(trustedResult);
resolved = true;
}
}
} else {
// we are all up to date, make sure to resolve if primaryResult is null
if (!resolved) {
resolve(null);
resolved = true;
}
}
}
await tryResolveTrustedPromise();
} catch (e) {
this.unverify();
if (!resolved) {
reject(e);
resolved = true;
} else {
console.warn('server request failed after returning cache value (or when already rejected)', e);
}
}
});
}
}