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 #9224

Merged
merged 3 commits into from
Feb 13, 2024
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
2 changes: 1 addition & 1 deletion packages/-ember-data/app/transforms/boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { deprecate } from '@ember/debug';
export { BooleanTransform as default } from '@ember-data/serializer/-private';

deprecate(
"You are relying on ember-data auto-magically installing the BooleanTransform. Use `export { BooleanTransform as default } from 'ember-data/serializer/transform';` in app/transforms/boolean.js instead",
"You are relying on ember-data auto-magically installing the BooleanTransform. Use `export { BooleanTransform as default } from '@ember-data/serializer/transform';` in app/transforms/boolean.js instead",
false,
{
id: 'ember-data:deprecate-legacy-imports',
Expand Down
2 changes: 1 addition & 1 deletion packages/-ember-data/app/transforms/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { deprecate } from '@ember/debug';
export { DateTransform as default } from '@ember-data/serializer/-private';

deprecate(
"You are relying on ember-data auto-magically installing the DateTransform. Use `export { DateTransform as default } from 'ember-data/serializer/transform';` in app/transforms/date.js instead",
"You are relying on ember-data auto-magically installing the DateTransform. Use `export { DateTransform as default } from '@ember-data/serializer/transform';` in app/transforms/date.js instead",
false,
{
id: 'ember-data:deprecate-legacy-imports',
Expand Down
2 changes: 1 addition & 1 deletion packages/-ember-data/app/transforms/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { deprecate } from '@ember/debug';
export { NumberTransform as default } from '@ember-data/serializer/-private';

deprecate(
"You are relying on ember-data auto-magically installing the NumberTransform. Use `export { NumberTransform as default } from 'ember-data/serializer/transform';` in app/transforms/number.js instead",
"You are relying on ember-data auto-magically installing the NumberTransform. Use `export { NumberTransform as default } from '@ember-data/serializer/transform';` in app/transforms/number.js instead",
false,
{
id: 'ember-data:deprecate-legacy-imports',
Expand Down
2 changes: 1 addition & 1 deletion packages/-ember-data/app/transforms/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { deprecate } from '@ember/debug';
export { StringTransform as default } from '@ember-data/serializer/-private';

deprecate(
"You are relying on ember-data auto-magically installing the StringTransform. Use `export { StringTransform as default } from 'ember-data/serializer/transform';` in app/transforms/string.js instead",
"You are relying on ember-data auto-magically installing the StringTransform. Use `export { StringTransform as default } from '@ember-data/serializer/transform';` in app/transforms/string.js instead",
false,
{
id: 'ember-data:deprecate-legacy-imports',
Expand Down
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);
}
}
98 changes: 94 additions & 4 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';

import type {
Expand All @@ -15,16 +18,91 @@ import type {
ResourceErrorDocument,
} from '@ember-data/types/cache/document';
import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';
import { Cache } from '@ember-data/types/q/cache';
import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api';
import type { JsonApiError } from '@ember-data/types/q/record-data-json-api';
import type { RecordInstance } from '@ember-data/types/q/record-instance';
import type { CreateRequestOptions, DeleteRequestOptions, UpdateRequestOptions } from '@ember-data/types/request';

import { Document } from './document';

/**
* 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
Loading