Skip to content

Commit

Permalink
source: enable control of cache behavior for source and query APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
zbigg committed Nov 28, 2024
1 parent 7b237d0 commit 338a08d
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## Not released

- add cache control mechanism for sources and query APIs

## 0.3

### 0.3.0
Expand Down
2 changes: 2 additions & 0 deletions src/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,5 +53,6 @@ export const query = async function (
headers,
errorContext,
maxLengthURL,
localCache,
});
};
42 changes: 38 additions & 4 deletions src/api/request-with-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@ 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<string, Promise<unknown>>();
const DEFAULT_REQUEST_CACHE = new Map<string, Promise<unknown>>();

export async function requestWithParameters<T = any>({
baseUrl,
parameters = {},
headers: customHeaders = {},
errorContext,
maxLengthURL = DEFAULT_MAX_LENGTH_URL,
localCache,
}: {
baseUrl: string;
parameters?: Record<string, unknown>;
headers?: Record<string, string>;
errorContext: APIErrorContext;
maxLengthURL?: number;
localCache?: LocalCacheOptions;
}): Promise<T> {
// Parameters added to all requests issued with `requestWithParameters()`.
// These parameters override parameters already in the base URL, but not
Expand All @@ -41,7 +44,14 @@ export async function requestWithParameters<T = any>({

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<T>;
}

Expand Down Expand Up @@ -73,14 +83,38 @@ export async function requestWithParameters<T = any>({
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<string, string>
) {
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<string, unknown>,
Expand Down
3 changes: 2 additions & 1 deletion src/sources/base-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function baseSource<UrlParameters extends Record<string, unknown>>(
}
}
const baseUrl = buildSourceUrl(mergedOptions);
const {clientId, maxLengthURL, format} = mergedOptions;
const {clientId, maxLengthURL, format, localCache} = mergedOptions;
const headers = {
Authorization: `Bearer ${options.accessToken}`,
...options.headers,
Expand All @@ -65,6 +65,7 @@ export async function baseSource<UrlParameters extends Record<string, unknown>>(
headers,
errorContext,
maxLengthURL,
localCache,
});

const dataUrl = mapInstantiation[format].url[0];
Expand Down
18 changes: 18 additions & 0 deletions src/sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Promise<unknown>>;
};

export type SourceOptions = SourceRequiredOptions &
Expand Down

0 comments on commit 338a08d

Please sign in to comment.