// 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 { public primaryPromise: Promise | null = null; public trustedPromise: Promise | 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, private trustedFunc: () => Promise, private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise ) {} static getChanges & { id: string }>(primaryResult: T[] | null, trustedResult: T[] | null): Changes { let changes: Changes = { 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(primaryResult: T[] | null, trustedResult: T[] | null, changes: Changes): 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>(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 & { id: string }>( primaryFunc: () => Promise, trustedFunc: () => Promise, changesFunc: (changesType: AutoVerifierChangesType, changes: Changes) => Promise ) { return new AutoVerifier( primaryFunc, trustedFunc, async (primaryResult: T[] | null, trustedResult: T[] | null) => { let changes = AutoVerifier.getChanges(primaryResult, trustedResult); let changesType = AutoVerifier.getListChangesType(primaryResult, trustedResult, changes); await changesFunc(changesType, changes); } ); } static createStandardSingleAutoVerifier>( primaryFunc: () => Promise, trustedFunc: () => Promise, changesFunc: (changesType: AutoVerifierChangesType, primaryResult: T | null, trustedResult: T | null) => Promise ) { return new AutoVerifier( primaryFunc, trustedFunc, async (primaryResult: T | null, trustedResult: T | null) => { let changesType = AutoVerifier.getSingleChangesType(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 async fetchAndVerifyIfNeeded(): Promise { return await new Promise(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 (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 | 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 | 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); } } }); } }