Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved lifetimes-service capabilities #9163

Merged
merged 2 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 32 additions & 16 deletions packages/request-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { assert } from '@ember/debug';
import { assert, deprecate } from '@ember/debug';

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';

type Store = {
cache: Cache;
};

/**
* Simple utility function to assist in url building,
* query params, and other common request operations.
Expand Down Expand Up @@ -604,7 +600,7 @@ export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires
* export class Store extends DataStore {
* constructor(args) {
* super(args);
* this.lifetimes = new LifetimesService(this, { apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 });
* this.lifetimes = new LifetimesService({ apiCacheSoftExpires: 30_000, apiCacheHardExpires: 60_000 });
* }
* }
* ```
Expand All @@ -613,22 +609,42 @@ export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires
* @public
* @module @ember-data/request-utils
*/
// TODO this doesn't get documented correctly on the website because it shares a class name
// with the interface expected by the Store service
export class LifetimesService {
declare store: Store;
declare config: LifetimesConfig;
constructor(store: Store, config: LifetimesConfig) {
this.store = store;
this.config = config;

constructor(config: LifetimesConfig) {
const _config = arguments.length === 1 ? config : (arguments[1] as unknown as LifetimesConfig);
deprecate(
`Passing a Store to the LifetimesService 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 LifetimesService`, _config);
assert(
`You must pass a apiCacheSoftExpires to the LifetimesService`,
typeof _config.apiCacheSoftExpires === 'number'
);
assert(
`You must pass a apiCacheHardExpires to the LifetimesService`,
typeof _config.apiCacheHardExpires === 'number'
);
this.config = _config;
}

isHardExpired(identifier: StableDocumentIdentifier): boolean {
const cached = this.store.cache.peekRequest(identifier);
isHardExpired(identifier: StableDocumentIdentifier, cache: Cache): boolean {
const cached = cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheHardExpires);
}
isSoftExpired(identifier: StableDocumentIdentifier): boolean {
const cached = this.store.cache.peekRequest(identifier);
isSoftExpired(identifier: StableDocumentIdentifier, cache: Cache): boolean {
const cached = cache.peekRequest(identifier);
return !cached || !cached.response || isStale(cached.response.headers, this.config.apiCacheSoftExpires);
}
}
98 changes: 94 additions & 4 deletions packages/store/src/-private/cache-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* @module @ember-data/store
*/
import { assert } from '@ember/debug';

import type { Future, Handler, NextFn } from '@ember-data/request/-private/types';
import type { Cache } from '@warp-drive/core-types/cache';
import type { StableDocumentIdentifier } from '@warp-drive/core-types/identifier';
import type {
CreateRequestOptions,
Expand All @@ -24,9 +28,83 @@ import type { RecordInstance } from '../-types/q/record-instance';
import { Document } from './document';
import type Store from './store-service';

/**
* A service which an application may provide to the store via
* the store's `lifetimes` property to configure the behavior
* of the CacheHandler.
*
* The default behavior for request lifetimes is to never expire
* unless manually refreshed via `cacheOptions.reload` or `cacheOptions.backgroundReload`.
*
* Implementing this service allows you to programatically define
* when a request should be considered expired.
*
* @class <Interface> LifetimesService
* @public
*/
export interface LifetimesService {
isHardExpired(identifier: StableDocumentIdentifier): boolean;
isSoftExpired(identifier: StableDocumentIdentifier): boolean;
/**
* Invoked to determine if the request may be fulfilled from cache
* if possible.
*
* Note, this is only invoked 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 {Cache} cache
* @returns {boolean} true if the request is considered hard expired
*/
isHardExpired(identifier: StableDocumentIdentifier, cache: Cache): boolean;
/**
* 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 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 {Cache} cache
* @returns {boolean} true if the request is considered soft expired
*/
isSoftExpired(identifier: StableDocumentIdentifier, cache: Cache): boolean;

/**
* Invoked when a request will be sent to the configured request handlers.
* This is invoked for both foreground and background requests.
*
* Note, this is only invoked if the request has a cache-key.
*
* @method willRequest [Optional]
* @public
* @param {StableDocumentIdentifier} identifier
* @param {Cache} cache
* @returns {void}
*/
willRequest?(identifier: StableDocumentIdentifier, cache: Cache): void;

/**
* Invoked when a request has been fulfilled from the configured request handlers.
* This is invoked for both foreground and background requests once the cache has
* been updated.
*
* Note, this is only invoked if the request has a cache-key.
*
* @method didRequest [Optional]
* @public
* @param {StableDocumentIdentifier} identifier
* @param {Cache} cache
* @returns {void}
*/
didRequest?(identifier: StableDocumentIdentifier, cache: Cache): void;
}

export type StoreRequestInfo = ImmutableRequestInfo;
Expand Down Expand Up @@ -170,7 +248,7 @@ function calcShouldFetch(
(request.op && MUTATION_OPS.has(request.op)) ||
cacheOptions?.reload ||
!hasCachedValue ||
(store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier) : false)
(store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier, store.cache) : false)
);
}

Expand All @@ -184,7 +262,7 @@ function calcShouldBackgroundFetch(
return (
!willFetch &&
(cacheOptions?.backgroundReload ||
(store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier) : false))
(store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier, store.cache) : false))
);
}

Expand Down Expand Up @@ -212,6 +290,10 @@ function fetchContentAndHydrate<T>(
store.cache.willCommit(record, context);
}

if (identifier && store.lifetimes?.willRequest) {
store.lifetimes.willRequest(identifier, store.cache);
}

const promise = next(context.request).then(
(document) => {
store.requestManager._pending.delete(context.id);
Expand All @@ -233,6 +315,10 @@ function fetchContentAndHydrate<T>(
});
store._enableAsyncFlush = null;

if (identifier && store.lifetimes?.didRequest) {
store.lifetimes.didRequest(identifier, store.cache);
}

if (shouldFetch) {
return response!;
} else if (shouldBackgroundFetch) {
Expand Down Expand Up @@ -275,6 +361,10 @@ function fetchContentAndHydrate<T>(
});
store._enableAsyncFlush = null;

if (identifier && store.lifetimes?.didRequest) {
store.lifetimes.didRequest(identifier, store.cache);
}

if (!shouldBackgroundFetch) {
const newError = cloneError(error);
newError.content = response;
Expand Down
4 changes: 4 additions & 0 deletions tests/docs/fixtures/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ module.exports = {
'(public) @ember-data/store IdentifierCache#getOrCreateDocumentIdentifier',
'(public) @ember-data/store Store#registerSchema',
'(public) @ember-data/store Store#schema',
'(public) @ember-data/store <Interface> LifetimesService#didRequest [Optional]',
'(public) @ember-data/store <Interface> LifetimesService#isHardExpired',
'(public) @ember-data/store <Interface> LifetimesService#isSoftExpired',
'(public) @ember-data/store <Interface> LifetimesService#willRequest [Optional]',
'(private) @ember-data/adapter BuildURLMixin#_buildURL',
'(private) @ember-data/adapter BuildURLMixin#urlPrefix',
'(private) @ember-data/adapter/json-api JSONAPIAdapter#ajaxOptions',
Expand Down
2 changes: 1 addition & 1 deletion tests/recommended-json-api/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class Store extends DataStore {
manager.useCache(CacheHandler);

this.registerSchema(buildSchema(this));
this.lifetimes = new LifetimesService(this, CONFIG);
this.lifetimes = new LifetimesService(CONFIG);
}

override createCache(capabilities: CacheCapabilitiesManager): Cache {
Expand Down
Loading