Skip to content

Commit

Permalink
feat: improved lifetimes-service capabilities (#9163)
Browse files Browse the repository at this point in the history
* feat: improved lifetimes-service capabilities

* fix lint
  • Loading branch information
runspired committed Feb 13, 2024
1 parent a04e060 commit 1d92ce8
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 20 deletions.
48 changes: 34 additions & 14 deletions packages/request-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assert } from '@ember/debug';
import { assert, deprecate } from '@ember/debug';

import type Store from '@ember-data/store';
import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';
import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';
import { Cache } from '@ember-data/types/q/cache';

/**
* Simple utility function to assist in url building,
Expand Down Expand Up @@ -602,7 +602,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 @@ -611,22 +611,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);
}
}
100 changes: 95 additions & 5 deletions packages/store/src/-private/cache-handler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @module @ember-data/store
*/
import { assert } from '@ember/debug';

Check failure on line 4 in packages/store/src/-private/cache-handler.ts

View workflow job for this annotation

GitHub Actions / lint

Run autofix to sort these imports!

import type {
Expand All @@ -21,10 +24,85 @@ import type { RecordInstance } from '@ember-data/types/q/record-instance';
import type { CreateRequestOptions, DeleteRequestOptions, UpdateRequestOptions } from '@ember-data/types/request';

import { Document } from './document';

import { Cache } from '@ember-data/types/q/cache';

/**
* 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 @@ -168,7 +246,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 @@ -182,7 +260,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 @@ -211,6 +289,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 @@ -232,6 +314,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 @@ -274,6 +360,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 @@ -33,6 +33,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);
}

createCache(capabilities: CacheCapabilitiesManager): Cache {
Expand Down

0 comments on commit 1d92ce8

Please sign in to comment.