Skip to content

Commit

Permalink
Allow cacheOptions/cacheOptionsFor to be async (#116)
Browse files Browse the repository at this point in the history
Not a huge fan of ValueOrPromise APIs but we already use them plenty in
this package.

Fixes #70.
  • Loading branch information
glasser authored Dec 9, 2022
1 parent be4371f commit ac767a7
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-crabs-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/datasource-rest': minor
---

The `cacheOptions` function and `cacheOptionsFor` method may now optionally be async.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ Allows setting the `CacheOptions` to be used for each request/response in the HT
You can also specify `cacheOptions` as part of the "request" in any call to `get()`, `post()`, etc. This can either be an object such as `{ttl: 1}`, or a function returning that object. If `cacheOptions` is provided, `cacheOptionsFor` is not called (ie, `this.cacheOptionsFor` is effectively the default value of `cacheOptions`).
The `cacheOptions` function and `cacheOptionsFor` method may be async.
```javascript
override cacheOptionsFor() {
return {
Expand Down
12 changes: 8 additions & 4 deletions src/HTTPCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
KeyValueCache,
PrefixingKeyValueCache,
} from '@apollo/utils.keyvaluecache';
import type { CacheOptions, RequestOptions } from './RESTDataSource';
import type {
CacheOptions,
RequestOptions,
ValueOrPromise,
} from './RESTDataSource';

// We want to use a couple internal properties of CachePolicy. (We could get
// `_url` and `_status` off of the serialized CachePolicyObject, but `age()` is
Expand Down Expand Up @@ -49,7 +53,7 @@ export class HTTPCache {
url: string,
response: FetcherResponse,
request: RequestOptions,
) => CacheOptions | undefined);
) => ValueOrPromise<CacheOptions | undefined>);
httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions;
},
): Promise<FetcherResponse> {
Expand Down Expand Up @@ -165,10 +169,10 @@ export class HTTPCache {
url: string,
response: FetcherResponse,
request: RequestOptions,
) => CacheOptions | undefined),
) => ValueOrPromise<CacheOptions | undefined>),
): Promise<FetcherResponse> {
if (typeof cacheOptions === 'function') {
cacheOptions = cacheOptions(url, response, request);
cacheOptions = await cacheOptions(url, response, request);
}

let ttlOverride = cacheOptions?.ttl;
Expand Down
16 changes: 8 additions & 8 deletions src/RESTDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import isPlainObject from 'lodash.isplainobject';
import { HTTPCache } from './HTTPCache';
import type { Options as HttpCacheSemanticsOptions } from 'http-cache-semantics';

type ValueOrPromise<T> = T | Promise<T>;
export type ValueOrPromise<T> = T | Promise<T>;

export type RequestOptions = FetcherRequestInit & {
/**
Expand All @@ -29,19 +29,19 @@ export type RequestOptions = FetcherRequestInit & {
*/
cacheKey?: string;
/**
* This can be a `CacheOptions` object or a function returning such an object.
* The details of what its fields mean are documented under `CacheOptions`.
* The function is called after a real HTTP request is made (and is not called
* if a response from the cache can be returned). If this is provided, the
* `cacheOptionsFor` hook is not called.
* This can be a `CacheOptions` object or a (possibly async) function
* returning such an object. The details of what its fields mean are
* documented under `CacheOptions`. The function is called after a real HTTP
* request is made (and is not called if a response from the cache can be
* returned). If this is provided, the `cacheOptionsFor` hook is not called.
*/
cacheOptions?:
| CacheOptions
| ((
url: string,
response: FetcherResponse,
request: RequestOptions,
) => CacheOptions | undefined);
) => Promise<CacheOptions | undefined>);
/**
* If provided, this is passed through as the third argument to `new
* CachePolicy()` from the `http-cache-semantics` npm package as part of the
Expand Down Expand Up @@ -207,7 +207,7 @@ export abstract class RESTDataSource {
url: string,
response: FetcherResponse,
request: FetcherRequestInit,
): CacheOptions | undefined;
): ValueOrPromise<CacheOptions | undefined>;

protected didEncounterError(error: Error, _request: RequestOptions) {
throw error;
Expand Down
29 changes: 28 additions & 1 deletion src/__tests__/HTTPCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('HTTPCache', () => {
afterEach(nockAfterEach);

beforeAll(() => {
// nock depends on process.nextTick
// nock depends on process.nextTick (and we use it to make async functions actually async)
jest.useFakeTimers({ doNotFake: ['nextTick'] });
});

Expand Down Expand Up @@ -218,6 +218,33 @@ describe('HTTPCache', () => {
expect(response.headers.get('age')).toEqual('10');
});

it('allows overriding the TTL dynamically with an async function', async () => {
mockGetAdaLovelace({
'cache-control': 'private, no-cache',
'set-cookie': 'foo',
});
await httpCache.fetch(
adaUrl,
{},
{
cacheOptions: async () => {
// Make it really async (using nextTick because we're not mocking it)
await new Promise<void>((resolve) => process.nextTick(resolve));
return {
ttl: 30,
};
},
},
);

jest.advanceTimersByTime(10000);

const response = await httpCache.fetch(adaUrl);

expect(await response.json()).toEqual({ name: 'Ada Lovelace' });
expect(response.headers.get('age')).toEqual('10');
});

it('allows disabling caching when the TTL is 0 (falsy)', async () => {
mockGetAdaLovelace({ 'cache-control': 'max-age=30' });

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/RESTDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@ describe('RESTDataSource', () => {
}

// Set a short TTL for every request
override cacheOptionsFor(): CacheOptions | undefined {
override async cacheOptionsFor(): Promise<CacheOptions | undefined> {
return {
ttl: 1,
};
Expand Down

0 comments on commit ac767a7

Please sign in to comment.