Skip to content

Commit 9b3aa9c

Browse files
committed
feat: improve lifetime handling of ad-hoc createRecord requests
1 parent c3d77e3 commit 9b3aa9c

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

packages/core-types/src/request.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ export type CacheOptions = {
4545
* provided by `@ember-data/request-utils` for an example.
4646
*
4747
* It is recommended to only use this for query/queryRecord requests where
48-
* new records created later would affect the results.
48+
* new records created later would affect the results, though using it for
49+
* findRecord requests is also supported if desired where it may be useful
50+
* when a create may affect the result of a sideloaded relationship.
51+
*
52+
* Generally it is better to patch the cache directly for relationship updates
53+
* than to invalidate findRecord requests for one.
4954
*
5055
* @typedoc
5156
*/

packages/request-utils/src/index.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -654,14 +654,25 @@ export type LifetimesConfig = { apiCacheSoftExpires: number; apiCacheHardExpires
654654
* Invalidates any request for which `cacheOptions.types` was provided when a createRecord
655655
* request for that type is successful.
656656
*
657+
* For this to work, the `createRecord` request must include the `cacheOptions.types` array
658+
* with the types that should be invalidated, or its request should specify the identifiers
659+
* of the records that are being created via `records`. Providing both is valid.
660+
*
661+
* > [!NOTE]
662+
* > only requests that had specified `cacheOptions.types` and occurred prior to the
663+
* > createRecord request will be invalidated. This means that a given request should always
664+
* > specify the types that would invalidate it to opt into this behavior. Abstracting this
665+
* > behavior via builders is recommended to ensure consistency.
666+
*
657667
* This allows the Store's CacheHandler to determine if a request is expired and
658668
* should be refetched upon next request.
659669
*
660670
* The `Fetch` handler provided by `@ember-data/request/fetch` will automatically
661671
* add the `date` header to responses if it is not present.
662672
*
663-
* Note: Date headers do not have millisecond precision, so expiration times should
664-
* generally be larger than 1000ms.
673+
* > [!NOTE]
674+
* > Date headers do not have millisecond precision, so expiration times should
675+
* > generally be larger than 1000ms.
665676
*
666677
* Usage:
667678
*
@@ -804,6 +815,11 @@ export class LifetimesService {
804815
const statusNumber = response?.status ?? 0;
805816
if (statusNumber >= 200 && statusNumber < 400) {
806817
const types = new Set(request.records?.map((r) => r.type));
818+
const additionalTypes = request.cacheOptions?.types;
819+
additionalTypes?.forEach((type) => {
820+
types.add(type);
821+
});
822+
807823
types.forEach((type) => {
808824
this.invalidateRequestsForType(type, store);
809825
});

packages/store/src/-private/cache-handler.ts

+41
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,47 @@ function cloneError(error: Error & { error: string | object }) {
411411
return cloned;
412412
}
413413

414+
/**
415+
* A CacheHandler that adds support for using an EmberData Cache with a RequestManager.
416+
*
417+
* This handler will only run when a request has supplied a `store` instance. Requests
418+
* issued by the store via `store.request()` will automatically have the `store` instance
419+
* attached to the request.
420+
*
421+
* ```ts
422+
* requestManager.request({
423+
* store: store,
424+
* url: '/api/posts',
425+
* method: 'GET'
426+
* });
427+
* ```
428+
*
429+
* When this handler elects to handle a request, it will return the raw `StructuredDocument`
430+
* unless the request has `[EnableHydration]` set to `true`. In this case, the handler will
431+
* return a `Document` instance that will automatically update the UI when the cache is updated
432+
* in the future and will hydrate any identifiers in the StructuredDocument into Record instances.
433+
*
434+
* When issuing a request via the store, [EnableHydration] is automatically set to `true`. This
435+
* means that if desired you can issue requests that utilize the cache without needing to also
436+
* utilize Record instances if desired.
437+
*
438+
* Said differently, you could elect to issue all requests via a RequestManager, without ever using
439+
* the store directly, by setting [EnableHydration] to `true` and providing a store instance. Not
440+
* necessarily the most useful thing, but the decoupled nature of the RequestManager and incremental-feature
441+
* approach of EmberData allows for this flexibility.
442+
*
443+
* ```ts
444+
* import { EnableHydration } from '@warp-drive/core-types/request';
445+
*
446+
* requestManager.request({
447+
* store: store,
448+
* url: '/api/posts',
449+
* method: 'GET',
450+
* [EnableHydration]: true
451+
* });
452+
*
453+
* @typedoc
454+
*/
414455
export const CacheHandler: CacheHandlerType = {
415456
request<T>(context: StoreRequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T>> | Future<T> | T {
416457
// if we have no cache or no cache-key skip cache handling

tests/main/tests/integration/cache-handler/lifetimes-test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ module('Store | CacheHandler + Lifetimes', function (hooks) {
300300

301301
assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still');
302302

303-
const record = store.createRecord('test', {}) as { identifier: StableRecordIdentifier };
303+
const record = store.createRecord('test', {});
304304

305305
await store.request({
306306
url: '/test',
@@ -477,7 +477,7 @@ module('Store | CacheHandler + Lifetimes', function (hooks) {
477477

478478
assert.verifySteps(['isHardExpired: false', 'isSoftExpired: false'], 'we resolve from cache still');
479479

480-
const record = store.createRecord('test', {}) as { identifier: StableRecordIdentifier };
480+
const record = store.createRecord('test', {});
481481
await store.saveRecord(record);
482482

483483
assert.verifySteps(['adapter:createRecord', 'didRequest'], 'we issue the request since it is a different request');
@@ -538,4 +538,6 @@ module('Store | CacheHandler + Lifetimes', function (hooks) {
538538
'we are no longer hard expired due to the createRecord response'
539539
);
540540
});
541+
542+
test('An AdHoc createRecord request can invalidate the request cache', async function (assert) {});
541543
});

0 commit comments

Comments
 (0)