cordis/client/webapp/auto-verifier.ts

183 lines
6.5 KiB
TypeScript
Raw Normal View History

import * as electronRemote from '@electron/remote';
const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../logger/logger';
import { Changes, WithEquals } from './data-types';
const LOG = Logger.create(__filename, electronConsole);
/**
* 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.
*/
export class AutoVerifier<T> {
private trustedPromise: Promise<T | null> | null = null;
private trustedStatus: 'none' | 'fetching' | 'verifying' | 'verified' = '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 createStandardListAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: () => Promise<T[] | null>,
trustedFunc: () => Promise<T[] | null>,
changesFunc: (changes: Changes<T>) => Promise<void>
) {
return new AutoVerifier<T[]>(
primaryFunc,
trustedFunc,
async (primaryResult: T[] | null, trustedResult: T[] | null) => {
await changesFunc(AutoVerifier.getChanges(primaryResult, trustedResult));
}
);
}
// You CAN safely call this while another fetch is going on! How convenient
unverify(): void {
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, reject) => {
let resolved = false;
try {
let primaryPromise = this.primaryFunc();
if (this.trustedStatus === 'none') {
this.trustedStatus = 'fetching';
this.trustedPromise = this.trustedFunc();
}
let primaryResult = await primaryPromise;
if (primaryResult) {
resolve(primaryResult);
resolved = true;
}
//@ts-ignore (could be changed by an unverify during primaryPromise)
if (this.trustedStatus === 'none') {
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!
LOG.warn('RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!');
await tryResolveTrustedPromise();
return;
}
if (!resolved) {
resolve(trustedResult);
resolved = true;
}
await this.verifyFunc(primaryResult, trustedResult);
if (this.trustedPromise === origTrustedPromise) {
this.trustedStatus = 'verified';
} else {
LOG.warn('RARE ALERT: we got unverified during verification!');
// We don't have to re-resolve since we already would have resolved with the correct trusted result
}
} 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!
LOG.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) {
if (!resolved) {
reject(e);
} else {
LOG.warn('server request failed after returning cache value', e);
}
}
});
}
}