Skip to content

Commit

Permalink
feat: allow server to respond 204-No Content after saveEntities (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
wardbell authored Oct 1, 2018
1 parent fb712f1 commit cbb9122
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 19 deletions.
22 changes: 16 additions & 6 deletions docs/save-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,26 @@ You need the results from the server to update the cache.
The server API is supposed to return all changed entity data in the
form of a `ChangeSet`.

The default `EntityCacheDataService.saveEntities()` implementation assumes that it does.
Often the server processes the saved entities without changing them.
There's no real need for the server to return the data.
The original request `ChangeSet` has all the information necessary to update the cache.
Responding with a `"204-No Content"` instead would save time, bandwidth, and processing.

The default `EntityCacheEffects.saveEntities$` dispatches
the `SAVE_ENTITIES_SUCCESS` action with these changes in
its `payload.changeSet`.
The server can respond `"204-No Content"` and send back nothing.
The `EntityCacheEffects` recognizes this condition and
returns a success action _derived_ from the original request `ChangeSet`.

If the save was pessimistic, it returns a `SaveEntitiesSuccess` action with the original `ChangeSet` in the payload.

If the save was optimistic, the changes are already in the cache and there's no point in updating the cache.
Instead, the effect returns a merge observable that clears the loading flags
for each entity type in the original `CacheSet`.

#### New _EntityOPs_ for multiple entity save

The `ChangeSet` in the payload of the `SAVE_ENTITIES_SUCCESS` has the save structure as
the `ChangeSet` in the `SAVE_ENTITIES` action, which was the source of the HTTP request.
When the server responds with a `ChangeSet`, or the effect re-uses the original request `ChangeSet`, the effect returns a `SAVE_ENTITIES_SUCCESS` action with the `ChangeSet` in the payload.

This `ChangeSet` has the save structure as the one in the `SAVE_ENTITIES` action, which was the source of the HTTP request.

The `EntityCacheReducer` converts the `ChangeSet.changes` into
a sequence of `EntityActions` to the entity collection reducers.
Expand Down
24 changes: 24 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ In other words, it is safer to have something like the following in your `packag
as this will keep you from installing `6.1.x`.

<hr>
<a id="6.1.0-alpha.3"></a>

# 6.1.0-alpha.3 (2018-12-02)

Non-breaking enhancements to _saveEntities_

The ngrx-data reducers that handle a successful save need a `ChangeSet` to update the cache.
For this reason, in prior versions, _the server had to respond with a_ `ChangeSet`.

Often the server processes the saved entities without changing them.
There's no need to return a result.
The original request `ChangeSet` has all the information necessary to update the cache.
Responding with a `"204-No Content"` instead would save time, bandwidth, and processing.

In this version, a server can respond `"204-No Content"` and send back nothing.
The `EntityCacheEffects` recognizes this condition and
returns a success action _derived_ from the original request `ChangeSet`.

If the save was pessimistic, it returns `SaveEntitiesSuccess` with the original `ChangeSet`.

If the save was optimistic, the changes are already in the cache and there's no point in updating the cache;
instead, the effect returns a merge observable that clears the loading flags for each entity type
in the original `CacheSet`.

<a id="6.1.0-alpha.2"></a>

# 6.1.0-alpha.2 (2018-09-19)
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngrx-data",
"version": "6.1.0-alpha.2",
"version": "6.1.0-alpha.3",
"repository": "https://github.com/johnpapa/angular-ngrx-data.git",
"license": "MIT",
"peerDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion lib/src/dataservices/entity-cache-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class EntityCacheDataService {
}
let hasMutated = false;
changes = changes.map(item => {
if (item.op === updateOp) {
if (item.op === updateOp && item.entities.length > 0) {
hasMutated = true;
return {
...item,
Expand All @@ -120,6 +120,10 @@ export class EntityCacheDataService {
* Reverse of flattenUpdates().
*/
protected restoreUpdates(changeSet: ChangeSet): ChangeSet {
if (changeSet == null) {
// Nothing? Server probably responded with 204 - No Content because it made no changes to the inserted or updated entities
return changeSet;
}
let changes = changeSet.changes;
if (changes.length === 0) {
return changeSet;
Expand Down
4 changes: 4 additions & 0 deletions lib/src/effects/entity-cache-effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ChangeSetUpdate
} from '../actions/entity-cache-change-set';
import { DataServiceError } from '../dataservices/data-service-error';
import { EntityActionFactory } from '../actions/entity-action-factory';
import { EntityCacheDataService } from '../dataservices/entity-cache-data.service';
import { EntityCacheDispatcher } from '../dispatchers/entity-cache-dispatcher';
import { EntityCacheEffects } from './entity-cache-effects';
Expand Down Expand Up @@ -51,9 +52,12 @@ describe('EntityCacheEffects (normal testing)', () => {
mergeStrategy = undefined;
options = { correlationId, mergeStrategy };

const eaFactory = new EntityActionFactory(); // doesn't change.

TestBed.configureTestingModule({
providers: [
EntityCacheEffects,
{ provide: EntityActionFactory, useValue: eaFactory },
{ provide: Actions, useValue: actions$ },
/* tslint:disable-next-line:no-use-before-declare */
{ provide: EntityCacheDataService, useClass: TestEntityCacheDataService },
Expand Down
52 changes: 47 additions & 5 deletions lib/src/effects/entity-cache-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { Inject, Injectable, Optional } from '@angular/core';
import { Action } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';

import { asyncScheduler, Observable, of, race, SchedulerLike } from 'rxjs';
import { catchError, delay, filter, map, mergeMap } from 'rxjs/operators';
import { asyncScheduler, Observable, of, merge, race, SchedulerLike } from 'rxjs';
import { concatMap, catchError, delay, filter, map, mergeMap } from 'rxjs/operators';

import { DataServiceError } from '../dataservices/data-service-error';
import { excludeEmptyChangeSetItems } from '../actions/entity-cache-change-set';
import { ChangeSet, excludeEmptyChangeSetItems } from '../actions/entity-cache-change-set';
import { EntityActionFactory } from '../actions/entity-action-factory';
import { EntityOp } from '../actions/entity-op';

import {
EntityCacheAction,
SaveEntities,
Expand All @@ -29,6 +32,7 @@ export class EntityCacheEffects {
constructor(
private actions: Actions,
private dataService: EntityCacheDataService,
private entityActionFactory: EntityActionFactory,
private logger: Logger,
/**
* Injecting an optional Scheduler that will be undefined
Expand Down Expand Up @@ -88,7 +92,9 @@ export class EntityCacheEffects {
const d = this.dataService
.saveEntities(changeSet, url)
.pipe(
map(result => new SaveEntitiesSuccess(result, url, options)),
concatMap(result =>
this.handleSaveEntitiesSuccess$(action, this.entityActionFactory)(result)
),
catchError(this.handleSaveEntitiesError$(action))
);

Expand All @@ -99,7 +105,7 @@ export class EntityCacheEffects {
}
}

/** Handle error result of saveEntities, returning a scalar observable of error action */
/** return handler of error result of saveEntities, returning a scalar observable of error action */
private handleSaveEntitiesError$(
action: SaveEntities
): (err: DataServiceError | Error) => Observable<Action> {
Expand All @@ -113,4 +119,40 @@ export class EntityCacheEffects {
);
};
}

/** return handler of the ChangeSet result of successful saveEntities() */
private handleSaveEntitiesSuccess$(
action: SaveEntities,
entityActionFactory: EntityActionFactory
): (changeSet: ChangeSet) => Observable<Action> {
const { url, correlationId, mergeStrategy, tag } = action.payload;
const options = { correlationId, mergeStrategy, tag };

return changeSet => {
// DataService returned a ChangeSet with possible updates to the saved entities
if (changeSet) {
return of(new SaveEntitiesSuccess(changeSet, url, options));
}

// No ChangeSet = Server probably responded '204 - No Content' because
// it made no changes to the inserted/updated entities.
// Respond with success action best on the ChangeSet in the request.
changeSet = action.payload.changeSet;

// If pessimistic save, return success action with the original ChangeSet
if (!action.payload.isOptimistic) {
return of(new SaveEntitiesSuccess(changeSet, url, options));
}

// If optimistic save, avoid cache grinding by just turning off the loading flags
// for all collections in the original ChangeSet
const entityNames = changeSet.changes.reduce(
(acc, item) => (acc.indexOf(item.entityName) === -1 ? acc.concat(item.entityName) : acc),
[] as string[]
);
return merge(
entityNames.map(name => entityActionFactory.create(name, EntityOp.SET_LOADING, false))
);
};
}
}
29 changes: 23 additions & 6 deletions src/in-memory-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
*/
import { Injectable } from '@angular/core';

import { RequestInfo, RequestInfoUtilities, ParsedRequestUrl, ResponseOptions, STATUS } from 'angular-in-memory-web-api';
import {
RequestInfo,
RequestInfoUtilities,
ParsedRequestUrl,
ResponseOptions,
STATUS
} from 'angular-in-memory-web-api';

import { ChangeSet } from 'ngrx-data';

Expand Down Expand Up @@ -88,14 +94,25 @@ export class InMemoryDataService {
}
});

// Respond with the changeSet in the request.
// That is a typical SaveEntities response.
// In this example, the server processes the request
// w/o changing the data that it inserts or updates.
// There's no reason to expect the server to return anything.
// So this API responds with 204-No Content and no body.
const options: ResponseOptions = {
url: requestInfo.url,
status: STATUS.OK,
statusText: 'OK',
body: changeSet
status: STATUS.NO_CONTENT,
statusText: 'NO CONTENT',
body: null
};

// But if it did return updated entities it might respond with this
// const options: ResponseOptions = {
// url: requestInfo.url,
// status: STATUS.OK,
// statusText: 'OK',
// body: changeSet
// };

return requestInfo.utils.createResponse$(() => options);
}
}
Expand Down

0 comments on commit cbb9122

Please sign in to comment.