Skip to content

Commit

Permalink
cache errors
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Nov 5, 2024
1 parent 8cfab2c commit 677fbf8
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 24 deletions.
83 changes: 77 additions & 6 deletions src/jsutils/__tests__/withCache-test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { expectPromise } from '../../__testUtils__/expectPromise.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

import { isPromise } from '../isPromise.js';
import { withCache } from '../withCache.js';

describe('withCache', () => {
it('returns asynchronously using asynchronous cache', async () => {
let cached: string | undefined;
let cached: string | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache = {
set: async (result: string) => {
set: async (result: string | Error) => {
await resolveOnNextTick();
cached = result;
},
Expand Down Expand Up @@ -46,11 +47,11 @@ describe('withCache', () => {
});

it('returns synchronously using cache with sync getter and async setter', async () => {
let cached: string | undefined;
let cached: string | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache = {
set: async (result: string) => {
set: async (result: string | Error) => {
await resolveOnNextTick();
cached = result;
},
Expand Down Expand Up @@ -80,11 +81,11 @@ describe('withCache', () => {
});

it('returns asynchronously using cache with async getter and sync setter', async () => {
let cached: string | undefined;
let cached: string | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache = {
set: (result: string) => {
set: (result: string | Error) => {
cached = result;
},
get: () => {
Expand Down Expand Up @@ -151,4 +152,74 @@ describe('withCache', () => {
expect(getAttempts).to.equal(2);
expect(cacheHits).to.equal(0);
});

it('caches fn errors with sync cache', () => {
let cached: string | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache = {
set: (result: string | Error) => {
cached = result;
},
get: () => {
getAttempts++;
if (cached !== undefined) {
cacheHits++;
}
return cached;
},
};

const fnWithCache = withCache((): string => {
throw new Error('Oops');
}, customCache);

expect(() => fnWithCache()).to.throw('Oops');

expect(getAttempts).to.equal(1);
expect(cacheHits).to.equal(0);

expect(() => fnWithCache()).to.throw('Oops');

expect(getAttempts).to.equal(2);
expect(cacheHits).to.equal(1);
});

it('caches fn errors with async cache', async () => {
let cached: string | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache = {
set: async (result: string | Error) => {
await resolveOnNextTick();
cached = result;
},
get: () => {
getAttempts++;
if (cached !== undefined) {
cacheHits++;
}
return Promise.resolve(cached);
},
};

const fnWithCache = withCache((): string => {
throw new Error('Oops');
}, customCache);

const firstResultPromise = fnWithCache();
expect(isPromise(firstResultPromise)).to.equal(true);

await expectPromise(firstResultPromise).toRejectWith('Oops');
expect(getAttempts).to.equal(1);
expect(cacheHits).to.equal(0);

const secondResultPromise = fnWithCache();

expect(isPromise(secondResultPromise)).to.equal(true);

await expectPromise(secondResultPromise).toRejectWith('Oops');
expect(getAttempts).to.equal(2);
expect(cacheHits).to.equal(1);
});
});
41 changes: 33 additions & 8 deletions src/jsutils/withCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { isPromise } from './isPromise.js';
import type { PromiseOrValue } from './PromiseOrValue.js';

export interface FnCache<
T extends (...args: Array<any>) => Exclude<any, undefined>,
T extends (...args: Array<any>) => Exclude<any, Error | undefined>,
> {
set: (result: ReturnType<T>, ...args: Parameters<T>) => PromiseOrValue<void>;
get: (...args: Parameters<T>) => PromiseOrValue<ReturnType<T> | undefined>;
set: (
result: ReturnType<T> | Error,
...args: Parameters<T>
) => PromiseOrValue<void>;
get: (
...args: Parameters<T>
) => PromiseOrValue<ReturnType<T> | Error | undefined>;
}

export function withCache<
T extends (...args: Array<any>) => Exclude<any, undefined>,
T extends (...args: Array<any>) => Exclude<any, Error | undefined>,
>(
fn: T,
cache: FnCache<T>,
Expand All @@ -27,23 +32,43 @@ export function withCache<
}

function handleCacheResult<
T extends (...args: Array<any>) => Exclude<any, undefined>,
T extends (...args: Array<any>) => Exclude<any, Error | undefined>,
>(
cachedResult: Awaited<ReturnType<T>> | undefined,
cachedResult: Awaited<ReturnType<T>> | Error | undefined,
fn: T,
cache: FnCache<T>,
args: Parameters<T>,
): Awaited<ReturnType<T>> {
if (cachedResult !== undefined) {
if (cachedResult instanceof Error) {
throw cachedResult;
}
return cachedResult;
}

const result = fn(...args);
let result;
try {
result = fn(...args);
} catch (error) {
updateResult(error, cache, args);
throw error;
}

updateResult(result, cache, args);
return result;
}

function updateResult<
T extends (...args: Array<any>) => Exclude<any, Error | undefined>,
>(
result: Awaited<ReturnType<T>> | Error,
cache: FnCache<T>,
args: Parameters<T>,
): void {
const setResult = cache.set(result, ...args);
if (isPromise(setResult)) {
setResult.catch(() => {
/* c8 ignore next */
});
}
return result;
}
4 changes: 2 additions & 2 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ export interface ParseOptions {

export interface ParseCache {
set: (
document: DocumentNode,
document: DocumentNode | Error,
source: string | Source,
options?: ParseOptions | undefined,
) => PromiseOrValue<void> | void;
get: (
source: string | Source,
options?: ParseOptions | undefined,
) => PromiseOrValue<DocumentNode | undefined>;
) => PromiseOrValue<DocumentNode | Error | undefined>;
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/validation/__tests__/validation-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache: ValidateCache = {
Expand Down Expand Up @@ -119,7 +119,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache: ValidateCache = {
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache: ValidateCache = {
Expand Down Expand Up @@ -226,7 +226,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache: ValidateCache = {
Expand Down Expand Up @@ -276,7 +276,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
let getAttempts = 0;
let cacheHits = 0;
const customCache: ValidateCache = {
Expand Down Expand Up @@ -329,7 +329,7 @@ describe('Validate: Supports full validation', () => {
}
`);

let cachedErrors: ReadonlyArray<GraphQLError> | undefined;
let cachedErrors: ReadonlyArray<GraphQLError> | Error | undefined;
const customCache: ValidateCache = {
set: async (resultedErrors) => {
await resolveOnNextTick();
Expand Down
4 changes: 2 additions & 2 deletions src/validation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface ValidateOptions {

export interface ValidateCache {
set: (
errors: ReadonlyArray<GraphQLError>,
errors: ReadonlyArray<GraphQLError> | Error,
schema: GraphQLSchema,
documentAST: DocumentNode,
rules?: ReadonlyArray<ValidationRule> | undefined,
Expand All @@ -39,7 +39,7 @@ export interface ValidateCache {
documentAST: DocumentNode,
rules?: ReadonlyArray<ValidationRule> | undefined,
options?: ValidateOptions | undefined,
) => PromiseOrValue<ReadonlyArray<GraphQLError> | undefined>;
) => PromiseOrValue<ReadonlyArray<GraphQLError> | Error | undefined>;
}

/**
Expand Down

0 comments on commit 677fbf8

Please sign in to comment.