import { deprecate } from '@ember/debug';
import { assert } from '@warp-drive/build-config/macros';
import type { Cache } from '@warp-drive/core-types/cache';
import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier';
import type { QueryParamsSerializationOptions, QueryParamsSource, Serializable } from '@warp-drive/core-types/params';
import type { ImmutableRequestInfo, ResponseInfo } from '@warp-drive/core-types/request';
type Store = {
cache: Cache;
};
/**
* Simple utility function to assist in url building,
* query params, and other common request operations.
*
* These primitives may be used directly or composed
* by request builders to provide a consistent interface
* for building requests.
*
* For instance:
*
* ```ts
* import { buildBaseURL, buildQueryParams } from '@ember-data/request-utils';
*
* const baseURL = buildBaseURL({
* host: 'https://api.example.com',
* namespace: 'api/v1',
* resourcePath: 'emberDevelopers',
* op: 'query',
* identifier: { type: 'ember-developer' }
* });
* const url = `${baseURL}?${buildQueryParams({ name: 'Chris', include:['pets'] })}`;
* // => 'https://api.example.com/api/v1/emberDevelopers?include=pets&name=Chris'
* ```
*
* This is useful, but not as useful as the REST request builder for query which is sugar
* over this (and more!):
*
* ```ts
* import { query } from '@ember-data/rest/request';
*
* const options = query('ember-developer', { name: 'Chris', include:['pets'] });
* // => { url: 'https://api.example.com/api/v1/emberDevelopers?include=pets&name=Chris' }
* // Note: options will also include other request options like headers, method, etc.
* ```
*
* @module @ember-data/request-utils
* @main @ember-data/request-utils
* @public
*/
// prevents the final constructed object from needing to add
// host and namespace which are provided by the final consuming
// class to the prototype which can result in overwrite errors
export interface BuildURLConfig {
host: string | null;
namespace: string | null;
}
const CONFIG: BuildURLConfig = {
host: '',
namespace: '',
};
/**
* Sets the global configuration for `buildBaseURL`
* for host and namespace values for the application.
*
* These values may still be overridden by passing
* them to buildBaseURL directly.
*
* This method may be called as many times as needed.
* host values of `''` or `'/'` are equivalent.
*
* Except for the value of `/` as host, host should not
* end with `/`.
*
* namespace should not start or end with a `/`.
*
* ```ts
* type BuildURLConfig = {
* host: string;
* namespace: string'
* }
* ```
*
* Example:
*
* ```ts
* import { setBuildURLConfig } from '@ember-data/request-utils';
*
* setBuildURLConfig({
* host: 'https://api.example.com',
* namespace: 'api/v1'
* });
* ```
*
* @method setBuildURLConfig
* @static
* @public
* @for @ember-data/request-utils
* @param {BuildURLConfig} config
* @return void
*/
export function setBuildURLConfig(config: BuildURLConfig) {
assert(`setBuildURLConfig: You must pass a config object`, config);
assert(
`setBuildURLConfig: You must pass a config object with a 'host' or 'namespace' property`,
'host' in config || 'namespace' in config
);
CONFIG.host = config.host || '';
CONFIG.namespace = config.namespace || '';
assert(
`buildBaseURL: host must NOT end with '/', received '${CONFIG.host}'`,
CONFIG.host === '/' || !CONFIG.host.endsWith('/')
);
assert(
`buildBaseURL: namespace must NOT start with '/', received '${CONFIG.namespace}'`,
!CONFIG.namespace.startsWith('/')
);
assert(
`buildBaseURL: namespace must NOT end with '/', received '${CONFIG.namespace}'`,
!CONFIG.namespace.endsWith('/')
);
}
export interface FindRecordUrlOptions {
op: 'findRecord';
identifier: { type: string; id: string };
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface QueryUrlOptions {
op: 'query';
identifier: { type: string };
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface FindManyUrlOptions {
op: 'findMany';
identifiers: { type: string; id: string }[];
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface FindRelatedCollectionUrlOptions {
op: 'findRelatedCollection';
identifier: { type: string; id: string };
fieldPath: string;
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface FindRelatedResourceUrlOptions {
op: 'findRelatedRecord';
identifier: { type: string; id: string };
fieldPath: string;
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface CreateRecordUrlOptions {
op: 'createRecord';
identifier: { type: string };
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface UpdateRecordUrlOptions {
op: 'updateRecord';
identifier: { type: string; id: string };
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface DeleteRecordUrlOptions {
op: 'deleteRecord';
identifier: { type: string; id: string };
resourcePath?: string;
host?: string;
namespace?: string;
}
export interface GenericUrlOptions {
resourcePath: string;
host?: string;
namespace?: string;
}
export type UrlOptions =
| FindRecordUrlOptions
| QueryUrlOptions
| FindManyUrlOptions
| FindRelatedCollectionUrlOptions
| FindRelatedResourceUrlOptions
| CreateRecordUrlOptions
| UpdateRecordUrlOptions
| DeleteRecordUrlOptions
| GenericUrlOptions;
const OPERATIONS_WITH_PRIMARY_RECORDS = new Set([
'findRecord',
'findRelatedRecord',
'findRelatedCollection',
'updateRecord',
'deleteRecord',
]);
function isOperationWithPrimaryRecord(
options: UrlOptions
): options is
| FindRecordUrlOptions
| FindRelatedCollectionUrlOptions
| FindRelatedResourceUrlOptions
| UpdateRecordUrlOptions
| DeleteRecordUrlOptions {
return 'op' in options && OPERATIONS_WITH_PRIMARY_RECORDS.has(options.op);
}
function hasResourcePath(options: UrlOptions): options is GenericUrlOptions {
return 'resourcePath' in options && typeof options.resourcePath === 'string' && options.resourcePath.length > 0;
}
function resourcePathForType(options: UrlOptions): string {
assert(
`resourcePathForType: You must pass a valid op as part of options`,
'op' in options && typeof options.op === 'string'
);
return options.op === 'findMany' ? options.identifiers[0].type : options.identifier.type;
}
/**
* Builds a URL for a request based on the provided options.
* Does not include support for building query params (see `buildQueryParams`)
* so that it may be composed cleanly with other query-params strategies.
*
* Usage:
*
* ```ts
* import { buildBaseURL } from '@ember-data/request-utils';
*
* const url = buildBaseURL({
* host: 'https://api.example.com',
* namespace: 'api/v1',
* resourcePath: 'emberDevelopers',
* op: 'query',
* identifier: { type: 'ember-developer' }
* });
*
* // => 'https://api.example.com/api/v1/emberDevelopers'
* ```
*
* On the surface this may seem like a lot of work to do something simple, but
* it is designed to be composable with other utilities and interfaces that the
* average product engineer will never need to see or use.
*
* A few notes:
*
* - `resourcePath` is optional, but if it is not provided, `identifier.type` will be used.
* - `host` and `namespace` are optional, but if they are not provided, the values globally
* configured via `setBuildURLConfig` will be used.
* - `op` is required and must be one of the following:
* - 'findRecord' 'query' 'findMany' 'findRelatedCollection' 'findRelatedRecord'` 'createRecord' 'updateRecord' 'deleteRecord'
* - Depending on the value of `op`, `identifier` or `identifiers` will be required.
*
* @method buildBaseURL
* @static
* @public
* @for @ember-data/request-utils
* @param urlOptions
* @return string
*/
export function buildBaseURL(urlOptions: UrlOptions): string {
const options = Object.assign(
{
host: CONFIG.host,
namespace: CONFIG.namespace,
},
urlOptions
);
assert(
`buildBaseURL: You must pass \`op\` as part of options`,
hasResourcePath(options) || (typeof options.op === 'string' && options.op.length > 0)
);
assert(
`buildBaseURL: You must pass \`identifier\` as part of options`,
hasResourcePath(options) ||
options.op === 'findMany' ||
(options.identifier && typeof options.identifier === 'object')
);
assert(
`buildBaseURL: You must pass \`identifiers\` as part of options`,
hasResourcePath(options) ||
options.op !== 'findMany' ||
(options.identifiers &&
Array.isArray(options.identifiers) &&
options.identifiers.length > 0 &&
options.identifiers.every((i) => i && typeof i === 'object'))
);
assert(
`buildBaseURL: You must pass valid \`identifier\` as part of options, expected 'id'`,
hasResourcePath(options) ||
!isOperationWithPrimaryRecord(options) ||
(typeof options.identifier.id === 'string' && options.identifier.id.length > 0)
);
assert(
`buildBaseURL: You must pass \`identifiers\` as part of options`,
hasResourcePath(options) ||
options.op !== 'findMany' ||
options.identifiers.every((i) => typeof i.id === 'string' && i.id.length > 0)
);
assert(
`buildBaseURL: You must pass valid \`identifier\` as part of options, expected 'type'`,
hasResourcePath(options) ||
options.op === 'findMany' ||
(typeof options.identifier.type === 'string' && options.identifier.type.length > 0)
);
assert(
`buildBaseURL: You must pass valid \`identifiers\` as part of options, expected 'type'`,
hasResourcePath(options) ||
options.op !== 'findMany' ||
(typeof options.identifiers[0].type === 'string' && options.identifiers[0].type.length > 0)
);
// prettier-ignore
const idPath: string =
isOperationWithPrimaryRecord(options) ? encodeURIComponent(options.identifier.id)
: '';
const resourcePath = options.resourcePath || resourcePathForType(options);
const { host, namespace } = options;
const fieldPath = 'fieldPath' in options ? options.fieldPath : '';
assert(
`buildBaseURL: You tried to build a url for a ${String(
'op' in options ? options.op + ' ' : ''
)}request to ${resourcePath} but resourcePath must be set or op must be one of "${[
'findRecord',
'findRelatedRecord',
'findRelatedCollection',
'updateRecord',
'deleteRecord',
'createRecord',
'query',
'findMany',
].join('","')}".`,
hasResourcePath(options) ||
[
'findRecord',
'query',
'findMany',
'findRelatedCollection',
'findRelatedRecord',
'createRecord',
'updateRecord',
'deleteRecord',
].includes(options.op)
);
assert(`buildBaseURL: host must NOT end with '/', received '${host}'`, host === '/' || !host.endsWith('/'));
assert(`buildBaseURL: namespace must NOT start with '/', received '${namespace}'`, !namespace.startsWith('/'));
assert(`buildBaseURL: namespace must NOT end with '/', received '${namespace}'`, !namespace.endsWith('/'));
assert(
`buildBaseURL: resourcePath must NOT start with '/', received '${resourcePath}'`,
!resourcePath.startsWith('/')
);
assert(`buildBaseURL: resourcePath must NOT end with '/', received '${resourcePath}'`, !resourcePath.endsWith('/'));
assert(`buildBaseURL: fieldPath must NOT start with '/', received '${fieldPath}'`, !fieldPath.startsWith('/'));
assert(`buildBaseURL: fieldPath must NOT end with '/', received '${fieldPath}'`, !fieldPath.endsWith('/'));
assert(`buildBaseURL: idPath must NOT start with '/', received '${idPath}'`, !idPath.startsWith('/'));
assert(`buildBaseURL: idPath must NOT end with '/', received '${idPath}'`, !idPath.endsWith('/'));
const hasHost = host !== '' && host !== '/';
const url = [hasHost ? host : '', namespace, resourcePath, idPath, fieldPath].filter(Boolean).join('/');
return hasHost ? url : `/${url}`;
}
const DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS: QueryParamsSerializationOptions = {
arrayFormat: 'comma',
};
function handleInclude(include: string | string[]): string[] {
assert(
`Expected include to be a string or array, got ${typeof include}`,
typeof include === 'string' || Array.isArray(include)
);
return typeof include === 'string' ? include.split(',') : include;
}
/**
* filter out keys of an object that have falsy values or point to empty arrays
* returning a new object with only those keys that have truthy values / non-empty arrays
*
* @method filterEmpty
* @static
* @public
* @for @ember-data/request-utils
* @param {Record<string, Serializable>} source object to filter keys with empty values from
* @return {Record<string, Serializable>} A new object with the keys that contained empty values removed
*/
export function filterEmpty(source: Record<string, Serializable>): Record<string, Serializable> {
const result: Record<string, Serializable> = {};
for (const key in source) {
const value = source[key];
// Allow `0` and `false` but filter falsy values that indicate "empty"
if (value !== undefined && value !== null && value !== '') {
if (!Array.isArray(value) || value.length > 0) {
result[key] = source[key];
}
}
}
return result;
}
/**
* Sorts query params by both key and value returning a new URLSearchParams
* object with the keys inserted in sorted order.
*
* Treats `included` specially, splicing it into an array if it is a string and sorting the array.
*
* Options:
* - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
*
* 'bracket': appends [] to the key for every value e.g. `&ids[]=1&ids[]=2`
* 'indices': appends [i] to the key for every value e.g. `&ids[0]=1&ids[1]=2`
* 'repeat': appends the key for every value e.g. `&ids=1&ids=2`
* 'comma' (default): appends the key once with a comma separated list of values e.g. `&ids=1,2`
*
* @method sortQueryParams
* @static
* @public
* @for @ember-data/request-utils
* @param {URLSearchParams | object} params
* @param {object} options
* @return {URLSearchParams} A URLSearchParams with keys inserted in sorted order
*/
export function sortQueryParams(params: QueryParamsSource, options?: QueryParamsSerializationOptions): URLSearchParams {
const opts = Object.assign({}, DEFAULT_QUERY_PARAMS_SERIALIZATION_OPTIONS, options);
const paramsIsObject = !(params instanceof URLSearchParams);
const urlParams = new URLSearchParams();
const dictionaryParams: Record<string, Serializable> = paramsIsObject ? params : {};
if (!paramsIsObject) {
params.forEach((value, key) => {
const hasExisting = key in dictionaryParams;
if (!hasExisting) {
dictionaryParams[key] = value;
} else {
const existingValue = dictionaryParams[key];
if (Array.isArray(existingValue)) {
existingValue.push(value);
} else {
dictionaryParams[key] = [existingValue, value];
}
}
});
}
if ('include' in dictionaryParams) {
dictionaryParams.include = handleInclude(dictionaryParams.include as string | string[]);
}
const sortedKeys = Object.keys(dictionaryParams).sort();
sortedKeys.forEach((key) => {
const value = dictionaryParams[key];
if (Array.isArray(value)) {
value.sort();
switch (opts.arrayFormat) {
case 'indices':
value.forEach((v, i) => {
urlParams.append(`${key}[${i}]`, String(v));
});
return;
case 'bracket':
value.forEach((v) => {
urlParams.append(`${key}[]`, String(v));
});
return;
case 'repeat':
value.forEach((v) => {
urlParams.append(key, String(v));
});
return;
case 'comma':
default:
urlParams.append(key, value.join(','));
return;
}
} else {
urlParams.append(key, String(value));
}
});
return urlParams;
}
/**
* Sorts query params by both key and value, returning a query params string
*
* Treats `included` specially, splicing it into an array if it is a string and sorting the array.
*
* Options:
* - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'
*
* 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2`
* 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2`
* 'repeat': appends the key for every value e.g. `ids=1&ids=2`
* 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2`
*
* @method buildQueryParams
* @static
* @public
* @for @ember-data/request-utils
* @param {URLSearchParams | object} params
* @param {object} [options]
* @return {string} A sorted query params string without the leading `?`
*/
export function buildQueryParams(params: QueryParamsSource, options?: QueryParamsSerializationOptions): string {
return sortQueryParams(params, options).toString();
}
export interface CacheControlValue {
immutable?: boolean;
'max-age'?: number;
'must-revalidate'?: boolean;
'must-understand'?: boolean;
'no-cache'?: boolean;
'no-store'?: boolean;
'no-transform'?: boolean;
'only-if-cached'?: boolean;
private?: boolean;
'proxy-revalidate'?: boolean;
public?: boolean;
's-maxage'?: number;
'stale-if-error'?: number;
'stale-while-revalidate'?: number;
}
type CacheControlKey = keyof CacheControlValue;
const NUMERIC_KEYS = new Set(['max-age', 's-maxage', 'stale-if-error', 'stale-while-revalidate']);
/**
* Parses a string Cache-Control header value into an object with the following structure:
*
* ```ts
* interface CacheControlValue {
* immutable?: boolean;
* 'max-age'?: number;
* 'must-revalidate'?: boolean;
* 'must-understand'?: boolean;
* 'no-cache'?: boolean;
* 'no-store'?: boolean;
* 'no-transform'?: boolean;
* 'only-if-cached'?: boolean;
* private?: boolean;
* 'proxy-revalidate'?: boolean;
* public?: boolean;
* 's-maxage'?: number;
* 'stale-if-error'?: number;
* 'stale-while-revalidate'?: number;
* }
* ```
* @method parseCacheControl
* @static
* @public
* @for @ember-data/request-utils
* @param {string} header
* @return {CacheControlValue}
*/
export function parseCacheControl(header: string): CacheControlValue {
let key: CacheControlKey = '' as CacheControlKey;
let value = '';
let isParsingKey = true;
const cacheControlValue: CacheControlValue = {};
function parseCacheControlValue(stringToParse: string): number {
const parsedValue = Number.parseInt(stringToParse);
assert(`Invalid Cache-Control value, expected a number but got - ${stringToParse}`, !Number.isNaN(parsedValue));
return parsedValue;
}
for (let i = 0; i < header.length; i++) {
const char = header.charAt(i);
if (char === ',') {
assert(`Invalid Cache-Control value, expected a value`, !isParsingKey || !NUMERIC_KEYS.has(key));
assert(
`Invalid Cache-Control value, expected a value after "=" but got ","`,
i === 0 || header.charAt(i - 1) !== '='
);
isParsingKey = true;
// @ts-expect-error TS incorrectly thinks that optional keys must have a type that includes undefined
cacheControlValue[key] = NUMERIC_KEYS.has(key) ? parseCacheControlValue(value) : true;
key = '' as CacheControlKey;
value = '';
continue;
} else if (char === '=') {
assert(`Invalid Cache-Control value, expected a value after "="`, i + 1 !== header.length);
isParsingKey = false;
} else if (char === ' ' || char === `\t` || char === `\n`) {
continue;
} else if (isParsingKey) {
key += char;
} else {
value += char;
}
if (i === header.length - 1) {
// @ts-expect-error TS incorrectly thinks that optional keys must have a type that includes undefined
cacheControlValue[key] = NUMERIC_KEYS.has(key) ? parseCacheControlValue(value) : true;
}
}
return cacheControlValue;
}
function isStale(headers: Headers, expirationTime: number): boolean {
// const age = headers.get('age');
// const cacheControl = parseCacheControl(headers.get('cache-control') || '');
// const expires = headers.get('expires');
// const lastModified = headers.get('last-modified');
const date = headers.get('date');
if (!date) {
return true;
}
const time = new Date(date).getTime();
const now = Date.now();
const deadline = time + expirationTime;
const result = now > deadline;
return result;
}
export type PolicyConfig = { apiCacheSoftExpires: number; apiCacheHardExpires: number };
/**
* A basic CachePolicy that can be added to the Store service.
*
* Determines staleness based on time since the request was last received from the API
* using the `date` header.
*
* Invalidates any request for which `cacheOptions.types` was provided when a createRecord
* request for that type is successful.
*
* For this to work, the `createRecord` request must include the `cacheOptions.types` array
* with the types that should be invalidated, or its request should specify the identifiers
* of the records that are being created via `records`. Providing both is valid.
*
* > [!NOTE]
* > only requests that had specified `cacheOptions.types` and occurred prior to the
* > createRecord request will be invalidated. This means that a given request should always
* > specify the types that would invalidate it to opt into this behavior. Abstracting this
* > behavior via builders is recommended to ensure consistency.
*
* This allows the Store's CacheHandler to determine if a request is expired and
* should be refetched upon next request.
*
* The `Fetch` handler provided by `@ember-data/request/fetch` will automatically
* add the `date` header to responses if it is not present.
*
* > [!NOTE]
* > Date headers do not have millisecond precision, so expiration times should
* > generally be larger than 1000ms.
*
* Usage:
*
* ```ts
* import { CachePolicy } from '@ember-data/request-utils';
* import DataStore from '@ember-data/store';
*
* // ...
*
* export class Store extends DataStore {
* constructor(args) {
* super(args);
* this.lifetimes = new CachePolicy({ apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 });
* }
* }
* ```
*
* @class CachePolicy
* @public
* @module @ember-data/request-utils
*/
export class CachePolicy {
declare config: PolicyConfig;
declare _stores: WeakMap<Store, { invalidated: Set<string>; types: Map<string, Set<string>> }>;
_getStore(store: Store): { invalidated: Set<string>; types: Map<string, Set<string>> } {
let set = this._stores.get(store);
if (!set) {
set = { invalidated: new Set(), types: new Map() };
this._stores.set(store, set);
}
return set;
}
constructor(config: PolicyConfig) {
this._stores = new WeakMap();
const _config = arguments.length === 1 ? config : (arguments[1] as unknown as PolicyConfig);
deprecate(
`Passing a Store to the CachePolicy is deprecated, please pass only a config instead.`,
arguments.length === 1,
{
id: 'ember-data:request-utils:lifetimes-service-store-arg',
since: {
enabled: '5.4',
available: '5.4',
},
for: '@ember-data/request-utils',
until: '6.0',
}
);
assert(`You must pass a config to the CachePolicy`, _config);
assert(`You must pass a apiCacheSoftExpires to the CachePolicy`, typeof _config.apiCacheSoftExpires === 'number');
assert(`You must pass a apiCacheHardExpires to the CachePolicy`, typeof _config.apiCacheHardExpires === 'number');
this.config = _config;
}
/**
* Invalidate a request by its identifier for a given store instance.
*
* While the store argument may seem redundant, the CachePolicy
* is designed to be shared across multiple stores / forks
* of the store.
*
* ```ts
* store.lifetimes.invalidateRequest(store, identifier);
* ```
*
* @method invalidateRequest
* @public
* @param {StableDocumentIdentifier} identifier
* @param {Store} store
*/
invalidateRequest(identifier: StableDocumentIdentifier, store: Store): void {
this._getStore(store).invalidated.add(identifier.lid);
}
/**
* Invalidate all requests associated to a specific type
* for a given store instance.
*
* While the store argument may seem redundant, the CachePolicy
* is designed to be shared across multiple stores / forks
* of the store.
*
* This invalidation is done automatically when using this service
* for both the CacheHandler and the LegacyNetworkHandler.
*
* ```ts
* store.lifetimes.invalidateRequestsForType(store, 'person');
* ```
*
* @method invalidateRequestsForType
* @public
* @param {string} type
* @param {Store} store
*/
invalidateRequestsForType(type: string, store: Store): void {
const storeCache = this._getStore(store);
const set = storeCache.types.get(type);
if (set) {
set.forEach((id) => {
storeCache.invalidated.add(id);
});
}
}
/**
* Invoked when a request has been fulfilled from the configured request handlers.
* This is invoked by the CacheHandler for both foreground and background requests
* once the cache has been updated.
*
* Note, this is invoked by the CacheHandler regardless of whether
* the request has a cache-key.
*
* This method should not be invoked directly by consumers.
*
* @method didRequest
* @public
* @param {ImmutableRequestInfo} request
* @param {ImmutableResponse} response
* @param {Store} store
* @param {StableDocumentIdentifier | null} identifier
* @return {void}
*/
didRequest(
request: ImmutableRequestInfo,
response: Response | ResponseInfo | null,
identifier: StableDocumentIdentifier | null,
store: Store
): void {
// if this is a successful createRecord request, invalidate the cacheKey for the type
if (request.op === 'createRecord') {
const statusNumber = response?.status ?? 0;
if (statusNumber >= 200 && statusNumber < 400) {
const types = new Set(request.records?.map((r) => r.type));
const additionalTypes = request.cacheOptions?.types;
additionalTypes?.forEach((type) => {
types.add(type);
});
types.forEach((type) => {
this.invalidateRequestsForType(type, store);
});
}
// add this document's cacheKey to a map for all associated types
// it is recommended to only use this for queries
} else if (identifier && request.cacheOptions?.types?.length) {
const storeCache = this._getStore(store);
request.cacheOptions?.types.forEach((type) => {
const set = storeCache.types.get(type);
if (set) {
set.add(identifier.lid);
storeCache.invalidated.delete(identifier.lid);
} else {
storeCache.types.set(type, new Set([identifier.lid]));
}
});
}
}
/**
* Invoked to determine if the request may be fulfilled from cache
* if possible.
*
* Note, this is only invoked by the CacheHandler if the request has
* a cache-key.
*
* If no cache entry is found or the entry is hard expired,
* the request will be fulfilled from the configured request handlers
* and the cache will be updated before returning the response.
*
* @method isHardExpired
* @public
* @param {StableDocumentIdentifier} identifier
* @param {Store} store
* @return {boolean} true if the request is considered hard expired
*/
isHardExpired(identifier: StableDocumentIdentifier, store: Store): boolean {
// if we are explicitly invalidated, we are hard expired
const storeCache = this._getStore(store);
if (storeCache.invalidated.has(identifier.lid)) {
return true;
}
const cache = store.cache;
const cached = cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheHardExpires);
}
/**
* Invoked if `isHardExpired` is false to determine if the request
* should be update behind the scenes if cache data is already available.
*
* Note, this is only invoked by the CacheHandler if the request has
* a cache-key.
*
* If true, the request will be fulfilled from cache while a backgrounded
* request is made to update the cache via the configured request handlers.
*
* @method isSoftExpired
* @public
* @param {StableDocumentIdentifier} identifier
* @param {Store} store
* @return {boolean} true if the request is considered soft expired
*/
isSoftExpired(identifier: StableDocumentIdentifier, store: Store): boolean {
const cache = store.cache;
const cached = cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheSoftExpires);
}
}
export class LifetimesService extends CachePolicy {
constructor(config: PolicyConfig) {
deprecate(
`\`import { LifetimesService } from '@ember-data/request-utils';\` is deprecated, please use \`import { CachePolicy } from '@ember-data/request-utils';\` instead.`,
false,
{
id: 'ember-data:deprecate-lifetimes-service-import',
since: {
enabled: '5.4',
available: '5.4',
},
for: 'ember-data',
until: '6.0',
}
);
super(config);
}
}