266 lines
10 KiB
TypeScript
266 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) {
|
|
if (!primaryElement.equals(trustedElement)) {
|
|
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 primary promise', e); });
|
|
}
|
|
if (this.trustedPromise) {
|
|
this.trustedPromise.catch((e) => { console.warn('caught unverified trusted 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
|
|
// @param lazyVerify: set to true to only verify if primaryResult returns null
|
|
async fetchAndVerifyIfNeeded(lazyVerify: boolean = false): Promise<T | null> {
|
|
return await new Promise<T | null>(async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
|
|
let resolved = false;
|
|
try {
|
|
if (this.primaryPromise === null) {
|
|
this.primaryPromise = this.primaryFunc();
|
|
}
|
|
let origPrimaryPromise = this.primaryPromise;
|
|
|
|
// pre-fetch the trusted result while we fetch the primary result
|
|
if (!lazyVerify && 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;
|
|
if (lazyVerify || this.trustedStatus === 'verified') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
//@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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|