remove ensureTrustedFuncReady, add ab primay null/dedup, trusted value/dedup

This commit is contained in:
Michael Peters 2022-10-06 22:59:09 -07:00
parent 54195fdf14
commit 5326f067be
2 changed files with 119 additions and 209 deletions

View File

@ -105,21 +105,21 @@ describe('getChanges tests', () => {
describe('constructor/factory tests', () => { describe('constructor/factory tests', () => {
test('constructor', () => { test('constructor', () => {
const av = new AutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); const av = new AutoVerifier(jest.fn(), jest.fn(), jest.fn());
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(null); expect(av.trustedPromise).toBe(null);
expect(av.trustedStatus).toBe('none') expect(av.trustedStatus).toBe('none')
}); });
test('createStandardListAutoVerifier', () => { test('createStandardListAutoVerifier', () => {
const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn());
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(null); expect(av.trustedPromise).toBe(null);
expect(av.trustedStatus).toBe('none') expect(av.trustedStatus).toBe('none')
}); });
test('createStandardSingleAutoVerifier', () => { test('createStandardSingleAutoVerifier', () => {
const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn(), jest.fn()); const av = AutoVerifier.createStandardListAutoVerifier(jest.fn(), jest.fn(), jest.fn());
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(null); expect(av.trustedPromise).toBe(null);
expect(av.trustedStatus).toBe('none') expect(av.trustedStatus).toBe('none')
@ -145,34 +145,28 @@ describe('fetchAndVerifyIfNeeded tests', () => {
function getManualsAndMocks(): { function getManualsAndMocks(): {
nextPrimary: () => ManualPromise<BasicWE | null>, nextPrimary: () => ManualPromise<BasicWE | null>,
nextTrusted: () => ManualPromise<BasicWE | null>, nextTrusted: () => ManualPromise<BasicWE | null>,
nextEnsureTrustedFuncReady: () => ManualPromise<void>,
nextVerify: () => ManualPromise<boolean>, nextVerify: () => ManualPromise<boolean>,
primaryFunc: jest.Mock<Promise<BasicWE | null>, []>, primaryFunc: jest.Mock<Promise<BasicWE | null>, []>,
trustedFunc: jest.Mock<Promise<BasicWE | null>, []>, trustedFunc: jest.Mock<Promise<BasicWE | null>, []>,
ensureTrustedFuncReadyFunc: jest.Mock<Promise<void>, []>,
verifyFunc: jest.Mock<Promise<boolean>, [primaryResult: BasicWE | null, trustedResult: BasicWE | null]>, verifyFunc: jest.Mock<Promise<boolean>, [primaryResult: BasicWE | null, trustedResult: BasicWE | null]>,
} { } {
const manuals: { const manuals: {
primary: ManualPromise<BasicWE | null>[], primary: ManualPromise<BasicWE | null>[],
trusted: ManualPromise<BasicWE | null>[], trusted: ManualPromise<BasicWE | null>[],
ensureTrustedFuncReady: ManualPromise<void>[],
verify: ManualPromise<boolean>[], verify: ManualPromise<boolean>[],
} = { } = {
primary: [], primary: [],
trusted: [], trusted: [],
ensureTrustedFuncReady: [],
verify: [], verify: [],
}; };
const calls: { const calls: {
primary: number, primary: number,
trusted: number, trusted: number,
ensureTrustedFuncReady: number,
verify: number, verify: number,
} = { } = {
primary: 0, primary: 0,
trusted: 0, trusted: 0,
ensureTrustedFuncReady: 0,
verify: 0 verify: 0
}; };
@ -185,11 +179,9 @@ describe('fetchAndVerifyIfNeeded tests', () => {
return { return {
nextPrimary: () => getNext(manuals.primary), nextPrimary: () => getNext(manuals.primary),
nextTrusted: () => getNext(manuals.trusted), nextTrusted: () => getNext(manuals.trusted),
nextEnsureTrustedFuncReady: () => getNext(manuals.ensureTrustedFuncReady),
nextVerify: () => getNext(manuals.verify), nextVerify: () => getNext(manuals.verify),
primaryFunc: jest.fn(() => manuals.primary[calls.primary++]!.promise), primaryFunc: jest.fn(() => manuals.primary[calls.primary++]!.promise),
trustedFunc: jest.fn(() => manuals.trusted[calls.trusted++]!.promise), trustedFunc: jest.fn(() => manuals.trusted[calls.trusted++]!.promise),
ensureTrustedFuncReadyFunc: jest.fn(() => manuals.ensureTrustedFuncReady[calls.ensureTrustedFuncReady++]!.promise),
verifyFunc: jest.fn((_primaryResult: BasicWE | null, _trustedResult: BasicWE | null) => manuals.verify[calls.verify++]!.promise), verifyFunc: jest.fn((_primaryResult: BasicWE | null, _trustedResult: BasicWE | null) => manuals.verify[calls.verify++]!.promise),
} }
} }
@ -198,21 +190,18 @@ describe('fetchAndVerifyIfNeeded tests', () => {
av: AutoVerifier<T>, av: AutoVerifier<T>,
nextPrimary: () => ManualPromise<T | null>, nextPrimary: () => ManualPromise<T | null>,
nextTrusted: () => ManualPromise<T | null>, nextTrusted: () => ManualPromise<T | null>,
nextEnsureTrustedFuncReady: () => ManualPromise<void>,
nextVerify: () => ManualPromise<boolean>, nextVerify: () => ManualPromise<boolean>,
lazyVerify: boolean = false, lazyVerify: boolean = false,
) { ) {
const state: { const state: {
primary: ManualPromise<T | null>, primary: ManualPromise<T | null>,
trusted: ManualPromise<T | null>, trusted: ManualPromise<T | null>,
ensureTrustedFuncReady: ManualPromise<void>,
verify: ManualPromise<boolean>, verify: ManualPromise<boolean>,
result: T | null | undefined, result: T | null | undefined,
rejection: Error | undefined, rejection: Error | undefined,
} = { } = {
primary: nextPrimary(), primary: nextPrimary(),
trusted: nextTrusted(), trusted: nextTrusted(),
ensureTrustedFuncReady: nextEnsureTrustedFuncReady(),
verify: nextVerify(), verify: nextVerify(),
result: undefined, result: undefined,
rejection: undefined, rejection: undefined,
@ -231,16 +220,15 @@ describe('fetchAndVerifyIfNeeded tests', () => {
test('primary null, then trusted with value - cache miss, return from server', async () => { test('primary null, then trusted with value - cache miss, return from server', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const verify = nextVerify(); const verify = nextVerify();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -251,18 +239,9 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(null); primary.resolve(null);
await disjoint(); await disjoint();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
@ -289,22 +268,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('primary with value, then trusted with value - cache hit, verify with server', async () => { test('primary with value, then trusted with value - cache hit, verify with server', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const verify = nextVerify(); const verify = nextVerify();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -316,19 +293,10 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(trustedFunc).toHaveBeenCalled(); // TODO: Is this the source of the problem? - trustedFunc needs to wait to be called until ensureTrustedFuncReadyFunc is resolved expect(trustedFunc).toHaveBeenCalled(); // TODO: Is this the source of the problem? - trustedFunc needs to wait to be called until ensureTrustedFuncReadyFunc is resolved
expect(result).toBeUndefined(); expect(result).toBeUndefined();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(new BasicWE(2)); primary.resolve(new BasicWE(2));
await disjoint(); await disjoint();
expect(result).toEqual(new BasicWE(2)); expect(result).toEqual(new BasicWE(2));
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
@ -353,22 +321,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('primary null, then trusted with null - cache miss, server confirms miss', async () => { test('primary null, then trusted with null - cache miss, server confirms miss', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const verify = nextVerify(); const verify = nextVerify();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -379,18 +345,9 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(null); primary.resolve(null);
await disjoint(); await disjoint();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
@ -417,22 +374,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('primary with value, then trusted with null - cache hit, server deleted', async () => { test('primary with value, then trusted with null - cache hit, server deleted', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const verify = nextVerify(); const verify = nextVerify();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -443,24 +398,15 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
expect(result).toBeUndefined(); expect(result).toBeUndefined();
primary.resolve(new BasicWE(2)); primary.resolve(new BasicWE(2));
await disjoint(); await disjoint();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(result).toEqual(new BasicWE(2)); expect(result).toEqual(new BasicWE(2));
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
trusted.resolve(null); trusted.resolve(null);
await disjoint(); await disjoint();
@ -481,22 +427,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('primary with null, lazy - lazy cache miss, need to ping server', async () => { test('primary with null, lazy - lazy cache miss, need to ping server', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const verify = nextVerify(); const verify = nextVerify();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(true); const resultPromise = av.fetchAndVerifyIfNeeded(true);
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -507,23 +451,14 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalledTimes(0); expect(trustedFunc).toHaveBeenCalledTimes(0);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(null); primary.resolve(null);
await disjoint(); await disjoint();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); // notably, we will never publicly see it in 'fetching' expect(av.trustedStatus).toBe('verifying'); // notably, we will never publicly see it in 'fetching'
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
trusted.resolve(new BasicWE(2)); trusted.resolve(new BasicWE(2));
await disjoint(); await disjoint();
@ -535,7 +470,6 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
expect(result).toBeUndefined(); expect(result).toBeUndefined();
@ -551,19 +485,18 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('primary with value, lazy - lazy cache hit, no need to ping server', async () => { test('primary with value, lazy - lazy cache hit, no need to ping server', async () => {
const { const {
nextPrimary, nextPrimary,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(true); const resultPromise = av.fetchAndVerifyIfNeeded(true);
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -584,7 +517,6 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(0); expect(trustedFunc).toHaveBeenCalledTimes(0);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
}); });
@ -592,13 +524,13 @@ describe('fetchAndVerifyIfNeeded tests', () => {
// expecting the promise to reject, the server to succeed, but the verify function to never be called // expecting the promise to reject, the server to succeed, but the verify function to never be called
const { const {
nextPrimary, nextTrusted, nextPrimary, nextTrusted,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let rejection: Error | undefined = undefined; let rejection: Error | undefined = undefined;
resultPromise.catch(value => { rejection = value; }); resultPromise.catch(value => { rejection = value; });
@ -623,22 +555,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
}); });
test('primary with null, trusted rejects - cache miss, failed to ping server', async () => { test('primary with null, trusted rejects - cache miss, failed to ping server', async () => {
// expect the promise to reject, but the verify function to never be called // expect the promise to reject, but the verify function to never be called
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextPrimary, nextTrusted,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let rejection: Error | undefined = undefined; let rejection: Error | undefined = undefined;
resultPromise.catch(value => { rejection = value; }); resultPromise.catch(value => { rejection = value; });
@ -649,18 +579,9 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(null); primary.resolve(null);
await disjoint(); await disjoint();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
@ -678,22 +599,20 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
}); });
test('primary with value, trusted rejects - cache hit, failed to ping server', async () => { test('primary with value, trusted rejects - cache hit, failed to ping server', async () => {
// expect the promise to reject, but the verify function to never be called // expect the promise to reject, but the verify function to never be called
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextPrimary, nextTrusted,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const primary = nextPrimary(); const primary = nextPrimary();
const trusted = nextTrusted(); const trusted = nextTrusted();
const ensureTrustedFuncReady = nextEnsureTrustedFuncReady();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const resultPromise = av.fetchAndVerifyIfNeeded(); const resultPromise = av.fetchAndVerifyIfNeeded();
let result: BasicWE | null | undefined = undefined; let result: BasicWE | null | undefined = undefined;
resultPromise.then(value => { result = value; }); resultPromise.then(value => { result = value; });
@ -705,19 +624,10 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
expect(result).toBeUndefined(); expect(result).toBeUndefined();
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(0);
primary.resolve(new BasicWE(2)); primary.resolve(new BasicWE(2));
await disjoint(); await disjoint();
expect(result).toEqual(new BasicWE(2)); expect(result).toEqual(new BasicWE(2));
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalled();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying');
ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null); expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(trusted.promise); expect(av.trustedPromise).toBe(trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
@ -736,19 +646,18 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out expect(trustedFunc).toHaveBeenCalledTimes(1); // notably, this server response will be thrown out
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
}); });
test('ab, a: primary with value, b: primary dedup, ab: trusted dedup with value', async () => { test('ab, a: primary with value, b: primary dedup, ab: trusted dedup with value', async () => {
const { const {
nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify, nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks(); } = getManualsAndMocks();
const av = new AutoVerifier(primaryFunc, trustedFunc, ensureTrustedFuncReadyFunc, verifyFunc); const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const a = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify); const a = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextVerify);
expect(av.primaryPromise).toBe(a.primary.promise); expect(av.primaryPromise).toBe(a.primary.promise);
expect(av.trustedPromise).toBe(a.trusted.promise); expect(av.trustedPromise).toBe(a.trusted.promise);
@ -756,7 +665,7 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(primaryFunc).toHaveBeenCalled(); expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled(); expect(trustedFunc).toHaveBeenCalled();
const b = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextEnsureTrustedFuncReady, nextVerify); const b = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextVerify);
expect(av.primaryPromise).toBe(a.primary.promise); // doesn't change from a expect(av.primaryPromise).toBe(a.primary.promise); // doesn't change from a
expect(av.trustedPromise).toBe(a.trusted.promise); // doesn't change from a expect(av.trustedPromise).toBe(a.trusted.promise); // doesn't change from a
@ -773,13 +682,6 @@ describe('fetchAndVerifyIfNeeded tests', () => {
expect(av.trustedPromise).toBe(a.trusted.promise); expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('verifying'); expect(av.trustedStatus).toBe('verifying');
a.ensureTrustedFuncReady.resolve();
await disjoint();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('verifying');
expect(verifyFunc).toHaveBeenCalledTimes(0); expect(verifyFunc).toHaveBeenCalledTimes(0);
a.trusted.resolve(new BasicWE(2)); a.trusted.resolve(new BasicWE(2));
await disjoint(); await disjoint();
@ -801,10 +703,65 @@ describe('fetchAndVerifyIfNeeded tests', () => {
// Even though there were two fetchAnd... calls, they were deduped such that each func was only called once // Even though there were two fetchAnd... calls, they were deduped such that each func was only called once
expect(primaryFunc).toHaveBeenCalledTimes(1); expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1); expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(ensureTrustedFuncReadyFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1); expect(verifyFunc).toHaveBeenCalledTimes(1);
}); });
test('ab, a: primary with null, b: primary dedup, ab: trusted dedup with value', async () => {
const {
nextPrimary, nextTrusted, nextVerify,
primaryFunc, trustedFunc, verifyFunc
} = getManualsAndMocks();
const av = new AutoVerifier(primaryFunc, trustedFunc, verifyFunc);
const a = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextVerify);
expect(av.primaryPromise).toBe(a.primary.promise);
expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('fetching');
expect(primaryFunc).toHaveBeenCalled();
expect(trustedFunc).toHaveBeenCalled();
const b = fetchAndVerifyIfNeededManually(av, nextPrimary, nextTrusted, nextVerify);
expect(av.primaryPromise).toBe(a.primary.promise); // doesn't change from a
expect(av.trustedPromise).toBe(a.trusted.promise); // doesn't change from a
expect(av.trustedStatus).toBe('fetching');
a.primary.resolve(null);
await disjoint();
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('verifying');
expect(verifyFunc).toHaveBeenCalledTimes(0);
a.trusted.resolve(new BasicWE(2));
await disjoint();
expect(verifyFunc).toHaveBeenCalledWith(null, new BasicWE(2));
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('verifying');
expect(a.result).toBeUndefined();
expect(b.result).toBeUndefined();
a.verify.resolve(true);
await disjoint();
expect(a.result).toEqual(new BasicWE(2));
expect(b.result).toEqual(new BasicWE(2));
expect(av.primaryPromise).toBe(null);
expect(av.trustedPromise).toBe(a.trusted.promise);
expect(av.trustedStatus).toBe('verified');
await disjoint(); // sanity check
// Even though there were two fetchAnd... calls, they were deduped such that each func was only called once
expect(primaryFunc).toHaveBeenCalledTimes(1);
expect(trustedFunc).toHaveBeenCalledTimes(1);
expect(verifyFunc).toHaveBeenCalledTimes(1);
});
// Make sure to do try/catch errors/rejections, verification failures, unverifies in the middle // Make sure to do try/catch errors/rejections, verification failures, unverifies in the middle
// Multiple tryResolveTrustedPromises - multiple fetchAndVerifyIfNeeded at the same time // Multiple tryResolveTrustedPromises - multiple fetchAndVerifyIfNeeded at the same time
// Trusted resolves *BEFORE* ensureTrustedFuncReady // Trusted resolves *BEFORE* ensureTrustedFuncReady

View File

@ -26,6 +26,7 @@ export enum AutoVerifierChangesType {
export class AutoVerifier<T> { export class AutoVerifier<T> {
public primaryPromise: Promise<T | null> | null = null; public primaryPromise: Promise<T | null> | null = null;
public trustedPromise: Promise<T | null> | null = null; public trustedPromise: Promise<T | null> | null = null;
public verifyPromise: Promise<boolean> | null = null;
public trustedStatus: 'none' | 'fetching' | 'verifying' | 'verified' = 'none'; public trustedStatus: 'none' | 'fetching' | 'verifying' | 'verified' = 'none';
private verifierId: string; private verifierId: string;
@ -39,7 +40,6 @@ export class AutoVerifier<T> {
constructor( constructor(
private primaryFunc: () => Promise<T | null>, private primaryFunc: () => Promise<T | null>,
private trustedFunc: () => Promise<T | null>, private trustedFunc: () => Promise<T | null>,
private ensureTrustedFuncReady: () => Promise<void>,
private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise<boolean>, private verifyFunc: (primaryResult: T | null, trustedResult: T | null) => Promise<boolean>,
) { ) {
this.verifierId = uuid.v4().slice(undefined, 4); this.verifierId = uuid.v4().slice(undefined, 4);
@ -123,13 +123,11 @@ export class AutoVerifier<T> {
static createStandardListAutoVerifier<T extends WithEquals<T> & { id: string }>( static createStandardListAutoVerifier<T extends WithEquals<T> & { id: string }>(
primaryFunc: () => Promise<T[] | null>, primaryFunc: () => Promise<T[] | null>,
trustedFunc: () => Promise<T[] | null>, trustedFunc: () => Promise<T[] | null>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: (changesType: AutoVerifierChangesType, changes: Changes<T>) => Promise<boolean>, changesFunc: (changesType: AutoVerifierChangesType, changes: Changes<T>) => Promise<boolean>,
) { ) {
return new AutoVerifier<T[]>( return new AutoVerifier<T[]>(
primaryFunc, primaryFunc,
trustedFunc, trustedFunc,
ensureTrustedFuncReady,
async (primaryResult: T[] | null, trustedResult: T[] | null) => { async (primaryResult: T[] | null, trustedResult: T[] | null) => {
const changes = AutoVerifier.getChanges<T>(primaryResult, trustedResult); const changes = AutoVerifier.getChanges<T>(primaryResult, trustedResult);
const changesType = AutoVerifier.getListChangesType<T>(primaryResult, trustedResult, changes); const changesType = AutoVerifier.getListChangesType<T>(primaryResult, trustedResult, changes);
@ -141,7 +139,6 @@ export class AutoVerifier<T> {
static createStandardSingleAutoVerifier<T extends WithEquals<T>>( static createStandardSingleAutoVerifier<T extends WithEquals<T>>(
primaryFunc: () => Promise<T | null>, primaryFunc: () => Promise<T | null>,
trustedFunc: () => Promise<T | null>, trustedFunc: () => Promise<T | null>,
ensureTrustedFuncReady: () => Promise<void>,
changesFunc: ( changesFunc: (
changesType: AutoVerifierChangesType, changesType: AutoVerifierChangesType,
primaryResult: T | null, primaryResult: T | null,
@ -151,7 +148,6 @@ export class AutoVerifier<T> {
return new AutoVerifier<T>( return new AutoVerifier<T>(
primaryFunc, primaryFunc,
trustedFunc, trustedFunc,
ensureTrustedFuncReady,
async (primaryResult: T | null, trustedResult: T | null) => { async (primaryResult: T | null, trustedResult: T | null) => {
const changesType = AutoVerifier.getSingleChangesType<T>(primaryResult, trustedResult); const changesType = AutoVerifier.getSingleChangesType<T>(primaryResult, trustedResult);
return await changesFunc(changesType, primaryResult, trustedResult); return await changesFunc(changesType, primaryResult, trustedResult);
@ -172,8 +168,14 @@ export class AutoVerifier<T> {
console.warn('caught unverified trusted promise', e); console.warn('caught unverified trusted promise', e);
}); });
} }
if (this.verifyPromise) {
this.verifyPromise.catch(e => {
console.warn('caught unverified verify promise', e);
});
}
this.primaryPromise = null; this.primaryPromise = null;
this.trustedPromise = null; this.trustedPromise = null;
this.verifyPromise = null;
this.trustedStatus = 'none'; this.trustedStatus = 'none';
} }
@ -181,62 +183,39 @@ export class AutoVerifier<T> {
// if the primary fetchable returns null but has not been verified yet, this will return the result of the trusted 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 // 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 (useful for fetching resources since they can never change) // @param lazyVerify: set to true to only verify if primaryResult returns null (useful for fetching resources since they can never change)
// @param debug: print debug messages. This is useful if you (unfortunately) think there is a bug in this async fetchAndVerifyIfNeeded(lazyVerify = false): Promise<T | null> {
async fetchAndVerifyIfNeeded(lazyVerify = false, debug = false): Promise<T | null> {
return await new Promise<T | null>( return await new Promise<T | null>(
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
async (resolve: (result: T | null) => void, reject: (error: Error) => void) => { async (resolve: (result: T | null) => void, reject: (error: Error) => void) => {
let resolved = false; let resolved = false;
/* istanbul ignore next */
const fetchId = debug && `v#${this.verifierId} f#${uuid.v4().slice(undefined, 4)}`;
try { try {
if (this.primaryPromise === null) { if (this.primaryPromise === null) {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': created primary promise');
this.primaryPromise = this.primaryFunc(); this.primaryPromise = this.primaryFunc();
} }
const origPrimaryPromise = this.primaryPromise; const origPrimaryPromise = this.primaryPromise;
// pre-fetch the trusted result while we fetch the primary result // pre-fetch the trusted result while we fetch the primary result
if (!lazyVerify && this.trustedStatus === 'none') { if (!lazyVerify && this.trustedStatus === 'none') {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ": created trusted promise, set to 'fetching'");
this.trustedStatus = 'fetching'; this.trustedStatus = 'fetching';
// TODO: The solution may be to merge the ensureTrustedFuncReady into the trustedPromise
this.trustedPromise = this.trustedFunc(); this.trustedPromise = this.trustedFunc();
} }
const primaryResult = await this.primaryPromise; const primaryResult = await this.primaryPromise;
if (this.primaryPromise === origPrimaryPromise) { if (this.primaryPromise === origPrimaryPromise) {
// reset the primary promise so we create a new one next time // reset the primary promise so we create a new one next time
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': reset the primary promise for next time');
this.primaryPromise = null; this.primaryPromise = null;
} }
if (primaryResult) { if (primaryResult) {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': resolving with primary result');
resolve(primaryResult); resolve(primaryResult);
resolved = true; resolved = true;
if (lazyVerify || this.trustedStatus === 'verified') { if (lazyVerify || this.trustedStatus === 'verified') {
/* istanbul ignore next */
if (debug)
LOG.debug(fetchId + ': not waiting on trusted promise', {
lazyVerify,
trustedStatus: this.trustedStatus,
});
return; return;
} }
} else {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': waiting for trusted to resolve');
} }
if (this.trustedStatus === 'none') { if (this.trustedStatus === 'none') {
// try to re-fetch the trusted result // try to re-fetch the trusted result
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ": creating trusted result (since status is 'none'");
this.trustedStatus = 'fetching'; this.trustedStatus = 'fetching';
this.trustedPromise = this.trustedFunc(); this.trustedPromise = this.trustedFunc();
} }
@ -249,12 +228,6 @@ export class AutoVerifier<T> {
// no one has started verifying the trusted yet // no one has started verifying the trusted yet
this.trustedStatus = 'verifying'; this.trustedStatus = 'verifying';
// TODO: I want to remove the ensureTrustedFuncReady func in favor
// of just including it yourself in the trustedFunc
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': verifying... (awaiting trustedPromise)');
await this.ensureTrustedFuncReady();
// if (debug) LOG.debug(fetchId + ': verifying...');
// note: Promises that have already resolved will return the same value when awaited again :) // note: Promises that have already resolved will return the same value when awaited again :)
const origTrustedPromise: Promise<T | null> | null = this.trustedPromise; const origTrustedPromise: Promise<T | null> | null = this.trustedPromise;
@ -263,17 +236,11 @@ export class AutoVerifier<T> {
if (this.trustedPromise !== origTrustedPromise) { if (this.trustedPromise !== origTrustedPromise) {
// we've been invalidated while we were waiting for the trusted result! // we've been invalidated while we were waiting for the trusted result!
// TODO: This happens when a socket fetch is sent before the socket is connected to. // TODO: This happens when a socket fetch is sent before the socket is connected to.
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': unverified during fetch!', new Error());
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': trustedPromise now: ', { trustedPromise: this.trustedPromise });
console.warn( console.warn(
'RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!', 'RARE ALERT: we got unverified while trying to fetch a trusted promise for verification!',
new Error(), new Error(),
); );
if (this.trustedPromise === null) { if (this.trustedPromise === null) {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': re-fetching since trustedPromise was null');
this.trustedStatus = 'fetching'; this.trustedStatus = 'fetching';
this.trustedPromise = this.trustedFunc(); this.trustedPromise = this.trustedFunc();
} }
@ -283,19 +250,16 @@ export class AutoVerifier<T> {
// make sure to verify BEFORE potentially resolving // make sure to verify BEFORE potentially resolving
// this way the conflicts can be resolved before the result is returned // this way the conflicts can be resolved before the result is returned
const primaryUpToDate = await this.verifyFunc(primaryResult, trustedResult); this.verifyPromise = this.verifyFunc(primaryResult, trustedResult);
const primaryUpToDate = await this.verifyPromise;
if (this.trustedPromise === origTrustedPromise) { if (this.trustedPromise === origTrustedPromise) {
if (trustedResult !== null && primaryUpToDate) { if (trustedResult !== null && primaryUpToDate) {
// we got a good trusted result and the primary data has been updated // we got a good trusted result and the primary data has been updated
// to reflect the trusted data (or already reflects it). // to reflect the trusted data (or already reflects it).
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': verified successfully.');
this.trustedStatus = 'verified'; this.trustedStatus = 'verified';
} else { } else {
// we have to re-fetch the trusted promise again next fetch // we have to re-fetch the trusted promise again next fetch
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': needs trusted promise re-fetched next time');
this.trustedStatus = 'none'; this.trustedStatus = 'none';
} }
} else { } else {
@ -307,8 +271,6 @@ export class AutoVerifier<T> {
if (!resolved) { if (!resolved) {
// removed 01/09/2021 pretty sure should not be here... && trustedResult // removed 01/09/2021 pretty sure should not be here... && trustedResult
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': resolving with trusted result');
resolve(trustedResult); resolve(trustedResult);
resolved = true; resolved = true;
} }
@ -317,22 +279,12 @@ export class AutoVerifier<T> {
// await the same trusted promise and return its result if we didn't get a result // await the same trusted promise and return its result if we didn't get a result
// from the primary source. // from the primary source.
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': waiting on result of a different verifier...');
// note: Promises that have already resolved will return the same value when awaited again :) // note: Promises that have already resolved will return the same value when awaited again :)
const origTrustedPromise: Promise<T | null> | null = this.trustedPromise; const origTrustedPromise: Promise<T | null> | null = this.trustedPromise;
const trustedResult = await origTrustedPromise; const trustedResult = await origTrustedPromise;
if (this.trustedPromise !== origTrustedPromise) { if (this.trustedPromise !== origTrustedPromise) {
// we've been invalidated while we were waiting for the trusted result! // we've been invalidated while we were waiting for the trusted result!
/* istanbul ignore next */
if (debug)
LOG.debug(
fetchId +
': got unverified while waiting on the result of a different verifier!',
new Error(),
);
console.warn( console.warn(
'ULTRA RARE ALERT: we got unverified while awaiting a trusted promise another path was verifying!', 'ULTRA RARE ALERT: we got unverified while awaiting a trusted promise another path was verifying!',
); );
@ -340,9 +292,20 @@ export class AutoVerifier<T> {
return; return;
} }
const origVerifyPromise: Promise<boolean> | null = this.verifyPromise;
await origVerifyPromise; // we don't care about the result, just that we wait for the verification to finish
if (this.verifyPromise !== origVerifyPromise) {
// we've been invalidated while we were waiting for the trusted result!
console.warn(
'ULTRA RARE ALERT: we got unverified while awaiting a verify promise another path was calling!',
origVerifyPromise,
this.verifyPromise,
);
await tryResolveTrustedPromise();
return;
}
if (!resolved) { if (!resolved) {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': resolving with trusted result of different verifier');
resolve(trustedResult); resolve(trustedResult);
resolved = true; resolved = true;
} }
@ -350,8 +313,6 @@ export class AutoVerifier<T> {
} else { } else {
// we are all up to date, make sure to resolve if primaryResult is null // we are all up to date, make sure to resolve if primaryResult is null
if (!resolved) { if (!resolved) {
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': no trusted promise, resolving with null');
resolve(null); resolve(null);
resolved = true; resolved = true;
} }
@ -362,21 +323,13 @@ export class AutoVerifier<T> {
if (!resolved) { if (!resolved) {
this.trustedPromise = null; // suppress warnings this.trustedPromise = null; // suppress warnings
this.primaryPromise = null; // suppress warnings this.primaryPromise = null; // suppress warnings
this.verifyPromise = null; // suppress warnings
this.unverify(); this.unverify();
/* istanbul ignore next */
if (debug) LOG.debug(fetchId + ': error during fetch', e);
// eslint-disable-next-line prefer-promise-reject-errors // eslint-disable-next-line prefer-promise-reject-errors
reject(e as Error); reject(e as Error);
resolved = true; resolved = true;
} else { } else {
this.unverify() this.unverify()
/* istanbul ignore next */
if (debug)
LOG.debug(
fetchId +
': server request failed after returning cache value (or we already rejected)',
e,
);
console.warn('server request failed after returning cache value (or when already rejected)', e); console.warn('server request failed after returning cache value (or when already rejected)', e);
} }
} }