From ac767a79a6bb6911703f04849b33553e3a872fef Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 9 Dec 2022 12:10:05 -0800 Subject: [PATCH] Allow cacheOptions/cacheOptionsFor to be async (#116) Not a huge fan of ValueOrPromise APIs but we already use them plenty in this package. Fixes #70. --- .changeset/twenty-crabs-tie.md | 5 +++++ README.md | 2 ++ src/HTTPCache.ts | 12 ++++++++---- src/RESTDataSource.ts | 16 +++++++-------- src/__tests__/HTTPCache.test.ts | 29 +++++++++++++++++++++++++++- src/__tests__/RESTDataSource.test.ts | 2 +- 6 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 .changeset/twenty-crabs-tie.md diff --git a/.changeset/twenty-crabs-tie.md b/.changeset/twenty-crabs-tie.md new file mode 100644 index 0000000..15239eb --- /dev/null +++ b/.changeset/twenty-crabs-tie.md @@ -0,0 +1,5 @@ +--- +'@apollo/datasource-rest': minor +--- + +The `cacheOptions` function and `cacheOptionsFor` method may now optionally be async. diff --git a/README.md b/README.md index 6f3091d..f8b3c2a 100644 --- a/README.md +++ b/README.md @@ -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 { diff --git a/src/HTTPCache.ts b/src/HTTPCache.ts index 652e179..90094a4 100644 --- a/src/HTTPCache.ts +++ b/src/HTTPCache.ts @@ -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 @@ -49,7 +53,7 @@ export class HTTPCache { url: string, response: FetcherResponse, request: RequestOptions, - ) => CacheOptions | undefined); + ) => ValueOrPromise); httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions; }, ): Promise { @@ -165,10 +169,10 @@ export class HTTPCache { url: string, response: FetcherResponse, request: RequestOptions, - ) => CacheOptions | undefined), + ) => ValueOrPromise), ): Promise { if (typeof cacheOptions === 'function') { - cacheOptions = cacheOptions(url, response, request); + cacheOptions = await cacheOptions(url, response, request); } let ttlOverride = cacheOptions?.ttl; diff --git a/src/RESTDataSource.ts b/src/RESTDataSource.ts index e3a3faa..b90e280 100644 --- a/src/RESTDataSource.ts +++ b/src/RESTDataSource.ts @@ -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 | Promise; +export type ValueOrPromise = T | Promise; export type RequestOptions = FetcherRequestInit & { /** @@ -29,11 +29,11 @@ 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 @@ -41,7 +41,7 @@ export type RequestOptions = FetcherRequestInit & { url: string, response: FetcherResponse, request: RequestOptions, - ) => CacheOptions | undefined); + ) => Promise); /** * If provided, this is passed through as the third argument to `new * CachePolicy()` from the `http-cache-semantics` npm package as part of the @@ -207,7 +207,7 @@ export abstract class RESTDataSource { url: string, response: FetcherResponse, request: FetcherRequestInit, - ): CacheOptions | undefined; + ): ValueOrPromise; protected didEncounterError(error: Error, _request: RequestOptions) { throw error; diff --git a/src/__tests__/HTTPCache.test.ts b/src/__tests__/HTTPCache.test.ts index 4ee1053..be54729 100644 --- a/src/__tests__/HTTPCache.test.ts +++ b/src/__tests__/HTTPCache.test.ts @@ -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'] }); }); @@ -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((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' }); diff --git a/src/__tests__/RESTDataSource.test.ts b/src/__tests__/RESTDataSource.test.ts index c144ed5..ba1a4a5 100644 --- a/src/__tests__/RESTDataSource.test.ts +++ b/src/__tests__/RESTDataSource.test.ts @@ -1089,7 +1089,7 @@ describe('RESTDataSource', () => { } // Set a short TTL for every request - override cacheOptionsFor(): CacheOptions | undefined { + override async cacheOptionsFor(): Promise { return { ttl: 1, };