From 7ae6cf9eace7b0e9089a0afc150af9850a6fb732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20Zag=C3=B3rski?= Date: Thu, 28 Nov 2024 15:45:48 +0100 Subject: [PATCH 1/2] source: enable control of cache behavior for source and query APIs --- CHANGELOG.md | 4 +++ src/api/query.ts | 2 ++ src/api/request-with-parameters.ts | 42 +++++++++++++++++++++++++++--- src/sources/base-source.ts | 5 +++- src/sources/types.ts | 18 +++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e75851e..fa9dd2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Not released + +- add cache control mechanism for sources and query APIs + ## 0.4 ### 0.4.0 diff --git a/src/api/query.ts b/src/api/query.ts index 3678038..b21b1b8 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -23,6 +23,7 @@ export const query = async function ( apiBaseUrl = SOURCE_DEFAULTS.apiBaseUrl, clientId = SOURCE_DEFAULTS.clientId, maxLengthURL = SOURCE_DEFAULTS.maxLengthURL, + localCache, connectionName, sqlQuery, queryParameters, @@ -52,5 +53,6 @@ export const query = async function ( headers, errorContext, maxLengthURL, + localCache, }); }; diff --git a/src/api/request-with-parameters.ts b/src/api/request-with-parameters.ts index 0cffe1a..1ae4514 100644 --- a/src/api/request-with-parameters.ts +++ b/src/api/request-with-parameters.ts @@ -7,13 +7,14 @@ import {CartoAPIError, APIErrorContext} from './carto-api-error'; import {V3_MINOR_VERSION} from '../constants-internal'; import {DEFAULT_MAX_LENGTH_URL} from '../constants-internal'; import {getClient} from '../client'; +import {LocalCacheOptions} from '../sources/types'; const DEFAULT_HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json', }; -const REQUEST_CACHE = new Map>(); +const DEFAULT_REQUEST_CACHE = new Map>(); export async function requestWithParameters({ baseUrl, @@ -21,12 +22,14 @@ export async function requestWithParameters({ headers: customHeaders = {}, errorContext, maxLengthURL = DEFAULT_MAX_LENGTH_URL, + localCache, }: { baseUrl: string; parameters?: Record; headers?: Record; errorContext: APIErrorContext; maxLengthURL?: number; + localCache?: LocalCacheOptions; }): Promise { // Parameters added to all requests issued with `requestWithParameters()`. // These parameters override parameters already in the base URL, but not @@ -41,7 +44,14 @@ export async function requestWithParameters({ baseUrl = excludeURLParameters(baseUrl, Object.keys(parameters)); const key = createCacheKey(baseUrl, parameters, customHeaders); - if (REQUEST_CACHE.has(key)) { + + const { + cache: REQUEST_CACHE, + canReadCache, + canStoreInCache, + } = getCacheSettings(localCache, customHeaders); + + if (canReadCache && REQUEST_CACHE.has(key)) { return REQUEST_CACHE.get(key) as Promise; } @@ -73,14 +83,38 @@ export async function requestWithParameters({ return json; }) .catch((error: Error) => { - REQUEST_CACHE.delete(key); + if (canStoreInCache) { + REQUEST_CACHE.delete(key); + } throw new CartoAPIError(error, errorContext, response, responseJson); }); - REQUEST_CACHE.set(key, jsonPromise); + if (canStoreInCache) { + REQUEST_CACHE.set(key, jsonPromise); + } return jsonPromise; } +function getCacheSettings( + localCache: LocalCacheOptions | undefined, + headers: Record +) { + const cacheControl = headers['Cache-Control']; + const canReadCache = localCache + ? localCache.canReadCache + : !cacheControl?.includes('no-cache'); + const canStoreInCache = localCache + ? localCache.canStoreInCache + : !cacheControl?.includes('no-store'); + const cache = localCache?.cache || DEFAULT_REQUEST_CACHE; + + return { + cache, + canReadCache, + canStoreInCache, + }; +} + function createCacheKey( baseUrl: string, parameters: Record, diff --git a/src/sources/base-source.ts b/src/sources/base-source.ts index 3b2f530..ac5878e 100644 --- a/src/sources/base-source.ts +++ b/src/sources/base-source.ts @@ -45,7 +45,7 @@ export async function baseSource>( } } const baseUrl = buildSourceUrl(mergedOptions); - const {clientId, maxLengthURL, format} = mergedOptions; + const {clientId, maxLengthURL, format, localCache} = mergedOptions; const headers = { Authorization: `Bearer ${options.accessToken}`, ...options.headers, @@ -65,6 +65,7 @@ export async function baseSource>( headers, errorContext, maxLengthURL, + localCache, }); const dataUrl = mapInstantiation[format].url[0]; @@ -82,6 +83,7 @@ export async function baseSource>( headers, errorContext, maxLengthURL, + localCache, }); if (accessToken) { json.accessToken = accessToken; @@ -94,5 +96,6 @@ export async function baseSource>( headers, errorContext, maxLengthURL, + localCache, }); } diff --git a/src/sources/types.ts b/src/sources/types.ts index 50ec91c..4ea1dd2 100644 --- a/src/sources/types.ts +++ b/src/sources/types.ts @@ -47,6 +47,24 @@ export type SourceOptionalOptions = { * @default {@link DEFAULT_MAX_LENGTH_URL} */ maxLengthURL?: number; + + /** + * Local cache options. + * * `canReadCache`: If `true`, the source will try to read from the local memory cache. + * * `canStoreInCache`: If `true`, the source will store the response in the local memory cache. + * * `cache`: A map of promises that are used to store the responses. + * + * If not provided, source will try to detect `CacheControl: no-cache or no-store` headers in the response and disable respective caching modes. + * + * By default, local in-memory caching is enabled + */ + localCache?: LocalCacheOptions; +}; + +export type LocalCacheOptions = { + canReadCache?: boolean; + canStoreInCache?: boolean; + cache?: Map>; }; export type SourceOptions = SourceRequiredOptions & From 7c11e428e4508830af0a6404442b21a216349899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20Zag=C3=B3rski?= Date: Mon, 2 Dec 2024 18:35:06 +0100 Subject: [PATCH 2/2] remove header interpretation, improve API --- src/api/request-with-parameters.ts | 20 ++++++++------------ src/sources/types.ts | 23 +++++++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/api/request-with-parameters.ts b/src/api/request-with-parameters.ts index 1ae4514..4261d17 100644 --- a/src/api/request-with-parameters.ts +++ b/src/api/request-with-parameters.ts @@ -49,7 +49,7 @@ export async function requestWithParameters({ cache: REQUEST_CACHE, canReadCache, canStoreInCache, - } = getCacheSettings(localCache, customHeaders); + } = getCacheSettings(localCache); if (canReadCache && REQUEST_CACHE.has(key)) { return REQUEST_CACHE.get(key) as Promise; @@ -95,17 +95,13 @@ export async function requestWithParameters({ return jsonPromise; } -function getCacheSettings( - localCache: LocalCacheOptions | undefined, - headers: Record -) { - const cacheControl = headers['Cache-Control']; - const canReadCache = localCache - ? localCache.canReadCache - : !cacheControl?.includes('no-cache'); - const canStoreInCache = localCache - ? localCache.canStoreInCache - : !cacheControl?.includes('no-store'); +function getCacheSettings(localCache: LocalCacheOptions | undefined) { + const canReadCache = localCache?.cacheControl?.includes('no-cache') + ? false + : true; + const canStoreInCache = localCache?.cacheControl?.includes('no-store') + ? false + : true; const cache = localCache?.cache || DEFAULT_REQUEST_CACHE; return { diff --git a/src/sources/types.ts b/src/sources/types.ts index 4ea1dd2..f61ec7d 100644 --- a/src/sources/types.ts +++ b/src/sources/types.ts @@ -49,22 +49,25 @@ export type SourceOptionalOptions = { maxLengthURL?: number; /** - * Local cache options. - * * `canReadCache`: If `true`, the source will try to read from the local memory cache. - * * `canStoreInCache`: If `true`, the source will store the response in the local memory cache. - * * `cache`: A map of promises that are used to store the responses. - * - * If not provided, source will try to detect `CacheControl: no-cache or no-store` headers in the response and disable respective caching modes. - * - * By default, local in-memory caching is enabled + * By default, local in-memory caching is enabled. */ localCache?: LocalCacheOptions; }; export type LocalCacheOptions = { - canReadCache?: boolean; - canStoreInCache?: boolean; + /** + * Map that stores requests and their responses. + */ cache?: Map>; + + /** + * Cache control + * * `no-cache`: If present, the source will always fetch from original source. + * * `no-store`: If present, source will not store result in cache (for later reuse). + * + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives + */ + cacheControl?: ('no-cache' | 'no-store')[]; }; export type SourceOptions = SourceRequiredOptions &