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 { private trustedPromise: Promise | 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, 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) { 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 & { id: string }>( primaryFunc: () => Promise, trustedFunc: () => Promise, changesFunc: (changes: Changes) => Promise ) { return new AutoVerifier( 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 { return await new Promise(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 | 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 | 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); } } }); } }