183 lines
6.5 KiB
TypeScript
183 lines
6.5 KiB
TypeScript
|
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);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|