Merge remote-tracking branch 'upstream/main' into feature-process_polling

# Conflicts:
#	src/app/core/data/base/base-data.service.spec.ts
This commit is contained in:
Alexandre Vryghem
2024-01-08 18:10:34 +01:00
11 changed files with 670 additions and 147 deletions

View File

@@ -101,6 +101,7 @@ describe('BaseDataService', () => {
remoteDataMocks = { remoteDataMocks = {
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
@@ -109,6 +110,7 @@ describe('BaseDataService', () => {
remoteDataPageMocks = { remoteDataPageMocks = {
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess), Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess),
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess), SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess),
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
@@ -317,19 +319,21 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', {
a: remoteDataMocks.SuccessStale, a: remoteDataMocks.ResponsePendingStale,
b: remoteDataMocks.RequestPending, b: remoteDataMocks.SuccessStale,
c: remoteDataMocks.ResponsePending, c: remoteDataMocks.ErrorStale,
d: remoteDataMocks.Success, d: remoteDataMocks.RequestPending,
e: remoteDataMocks.SuccessStale, e: remoteDataMocks.ResponsePending,
f: remoteDataMocks.Success,
g: remoteDataMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '------d-e-f-g';
const values = { const values = {
b: remoteDataMocks.RequestPending, d: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending, e: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success, f: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale, g: remoteDataMocks.SuccessStale,
}; };
expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values);
@@ -368,19 +372,21 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', {
a: remoteDataMocks.SuccessStale, a: remoteDataMocks.ResponsePendingStale,
b: remoteDataMocks.RequestPending, b: remoteDataMocks.SuccessStale,
c: remoteDataMocks.ResponsePending, c: remoteDataMocks.ErrorStale,
d: remoteDataMocks.Success, d: remoteDataMocks.RequestPending,
e: remoteDataMocks.SuccessStale, e: remoteDataMocks.ResponsePending,
f: remoteDataMocks.Success,
g: remoteDataMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '------d-e-f-g';
const values = { const values = {
b: remoteDataMocks.RequestPending, d: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending, e: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success, f: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale, g: remoteDataMocks.SuccessStale,
}; };
expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values);
@@ -522,19 +528,21 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
a: remoteDataPageMocks.SuccessStale, a: remoteDataPageMocks.ResponsePendingStale,
b: remoteDataPageMocks.RequestPending, b: remoteDataPageMocks.SuccessStale,
c: remoteDataPageMocks.ResponsePending, c: remoteDataPageMocks.ErrorStale,
d: remoteDataPageMocks.Success, d: remoteDataPageMocks.RequestPending,
e: remoteDataPageMocks.SuccessStale, e: remoteDataPageMocks.ResponsePending,
f: remoteDataPageMocks.Success,
g: remoteDataPageMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '------d-e-f-g';
const values = { const values = {
b: remoteDataPageMocks.RequestPending, d: remoteDataPageMocks.RequestPending,
c: remoteDataPageMocks.ResponsePending, e: remoteDataPageMocks.ResponsePending,
d: remoteDataPageMocks.Success, f: remoteDataPageMocks.Success,
e: remoteDataPageMocks.SuccessStale, g: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
@@ -573,21 +581,24 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', {
a: remoteDataPageMocks.SuccessStale, a: remoteDataPageMocks.ResponsePendingStale,
b: remoteDataPageMocks.RequestPending, b: remoteDataPageMocks.SuccessStale,
c: remoteDataPageMocks.ResponsePending, c: remoteDataPageMocks.ErrorStale,
d: remoteDataPageMocks.Success, d: remoteDataPageMocks.RequestPending,
e: remoteDataPageMocks.SuccessStale, e: remoteDataPageMocks.ResponsePending,
f: remoteDataPageMocks.Success,
g: remoteDataPageMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '------d-e-f-g';
const values = { const values = {
b: remoteDataPageMocks.RequestPending, d: remoteDataPageMocks.RequestPending,
c: remoteDataPageMocks.ResponsePending, e: remoteDataPageMocks.ResponsePending,
d: remoteDataPageMocks.Success, f: remoteDataPageMocks.Success,
e: remoteDataPageMocks.SuccessStale, g: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
}); });
}); });

View File

@@ -274,7 +274,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
// call it isn't immediately returned, but we wait until the remote data for the new request // call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
// cached completed object // cached completed object
skipWhile((rd: RemoteData<T>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), skipWhile((rd: RemoteData<T>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)),
this.reRequestStaleRemoteData(reRequestOnStale, () => this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
); );
@@ -324,7 +324,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
// call it isn't immediately returned, but we wait until the remote data for the new request // call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
// cached completed object // cached completed object
skipWhile((rd: RemoteData<PaginatedList<T>>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), skipWhile((rd: RemoteData<PaginatedList<T>>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)),
this.reRequestStaleRemoteData(reRequestOnStale, () => this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
); );

View File

@@ -0,0 +1,186 @@
import {
isRequestPending,
isError,
isSuccess,
isErrorStale,
isSuccessStale,
isResponsePending,
isResponsePendingStale,
isLoading,
isStale,
hasFailed,
hasSucceeded,
hasCompleted,
RequestEntryState
} from './request-entry-state.model';
describe(`isRequestPending`, () => {
it(`should only return true if the given state is RequestPending`, () => {
expect(isRequestPending(RequestEntryState.RequestPending)).toBeTrue();
expect(isRequestPending(RequestEntryState.ResponsePending)).toBeFalse();
expect(isRequestPending(RequestEntryState.Error)).toBeFalse();
expect(isRequestPending(RequestEntryState.Success)).toBeFalse();
expect(isRequestPending(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isRequestPending(RequestEntryState.ErrorStale)).toBeFalse();
expect(isRequestPending(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isError`, () => {
it(`should only return true if the given state is Error`, () => {
expect(isError(RequestEntryState.Error)).toBeTrue();
expect(isError(RequestEntryState.RequestPending)).toBeFalse();
expect(isError(RequestEntryState.ResponsePending)).toBeFalse();
expect(isError(RequestEntryState.Success)).toBeFalse();
expect(isError(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isError(RequestEntryState.ErrorStale)).toBeFalse();
expect(isError(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isSuccess`, () => {
it(`should only return true if the given state is Success`, () => {
expect(isSuccess(RequestEntryState.Success)).toBeTrue();
expect(isSuccess(RequestEntryState.RequestPending)).toBeFalse();
expect(isSuccess(RequestEntryState.ResponsePending)).toBeFalse();
expect(isSuccess(RequestEntryState.Error)).toBeFalse();
expect(isSuccess(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isSuccess(RequestEntryState.ErrorStale)).toBeFalse();
expect(isSuccess(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isErrorStale`, () => {
it(`should only return true if the given state is ErrorStale`, () => {
expect(isErrorStale(RequestEntryState.ErrorStale)).toBeTrue();
expect(isErrorStale(RequestEntryState.RequestPending)).toBeFalse();
expect(isErrorStale(RequestEntryState.ResponsePending)).toBeFalse();
expect(isErrorStale(RequestEntryState.Error)).toBeFalse();
expect(isErrorStale(RequestEntryState.Success)).toBeFalse();
expect(isErrorStale(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isErrorStale(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isSuccessStale`, () => {
it(`should only return true if the given state is SuccessStale`, () => {
expect(isSuccessStale(RequestEntryState.SuccessStale)).toBeTrue();
expect(isSuccessStale(RequestEntryState.RequestPending)).toBeFalse();
expect(isSuccessStale(RequestEntryState.ResponsePending)).toBeFalse();
expect(isSuccessStale(RequestEntryState.Error)).toBeFalse();
expect(isSuccessStale(RequestEntryState.Success)).toBeFalse();
expect(isSuccessStale(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isSuccessStale(RequestEntryState.ErrorStale)).toBeFalse();
});
});
describe(`isResponsePending`, () => {
it(`should only return true if the given state is ResponsePending`, () => {
expect(isResponsePending(RequestEntryState.ResponsePending)).toBeTrue();
expect(isResponsePending(RequestEntryState.RequestPending)).toBeFalse();
expect(isResponsePending(RequestEntryState.Error)).toBeFalse();
expect(isResponsePending(RequestEntryState.Success)).toBeFalse();
expect(isResponsePending(RequestEntryState.ResponsePendingStale)).toBeFalse();
expect(isResponsePending(RequestEntryState.ErrorStale)).toBeFalse();
expect(isResponsePending(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isResponsePendingStale`, () => {
it(`should only return true if the given state is requestPending`, () => {
expect(isResponsePendingStale(RequestEntryState.ResponsePendingStale)).toBeTrue();
expect(isResponsePendingStale(RequestEntryState.RequestPending)).toBeFalse();
expect(isResponsePendingStale(RequestEntryState.ResponsePending)).toBeFalse();
expect(isResponsePendingStale(RequestEntryState.Error)).toBeFalse();
expect(isResponsePendingStale(RequestEntryState.Success)).toBeFalse();
expect(isResponsePendingStale(RequestEntryState.ErrorStale)).toBeFalse();
expect(isResponsePendingStale(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`isLoading`, () => {
it(`should only return true if the given state is RequestPending, ResponsePending or ResponsePendingStale`, () => {
expect(isLoading(RequestEntryState.RequestPending)).toBeTrue();
expect(isLoading(RequestEntryState.ResponsePending)).toBeTrue();
expect(isLoading(RequestEntryState.ResponsePendingStale)).toBeTrue();
expect(isLoading(RequestEntryState.Error)).toBeFalse();
expect(isLoading(RequestEntryState.Success)).toBeFalse();
expect(isLoading(RequestEntryState.ErrorStale)).toBeFalse();
expect(isLoading(RequestEntryState.SuccessStale)).toBeFalse();
});
});
describe(`hasFailed`, () => {
describe(`when the state is loading`, () => {
it(`should return undefined`, () => {
expect(hasFailed(RequestEntryState.RequestPending)).toBeUndefined();
expect(hasFailed(RequestEntryState.ResponsePending)).toBeUndefined();
expect(hasFailed(RequestEntryState.ResponsePendingStale)).toBeUndefined();
});
});
describe(`when the state has completed`, () => {
it(`should only return true if the given state is Error or ErrorStale`, () => {
expect(hasFailed(RequestEntryState.Error)).toBeTrue();
expect(hasFailed(RequestEntryState.ErrorStale)).toBeTrue();
expect(hasFailed(RequestEntryState.Success)).toBeFalse();
expect(hasFailed(RequestEntryState.SuccessStale)).toBeFalse();
});
});
});
describe(`hasSucceeded`, () => {
describe(`when the state is loading`, () => {
it(`should return undefined`, () => {
expect(hasSucceeded(RequestEntryState.RequestPending)).toBeUndefined();
expect(hasSucceeded(RequestEntryState.ResponsePending)).toBeUndefined();
expect(hasSucceeded(RequestEntryState.ResponsePendingStale)).toBeUndefined();
});
});
describe(`when the state has completed`, () => {
it(`should only return true if the given state is Error or ErrorStale`, () => {
expect(hasSucceeded(RequestEntryState.Success)).toBeTrue();
expect(hasSucceeded(RequestEntryState.SuccessStale)).toBeTrue();
expect(hasSucceeded(RequestEntryState.Error)).toBeFalse();
expect(hasSucceeded(RequestEntryState.ErrorStale)).toBeFalse();
});
});
});
describe(`hasCompleted`, () => {
it(`should only return true if the given state is Error, Success, ErrorStale or SuccessStale`, () => {
expect(hasCompleted(RequestEntryState.Error)).toBeTrue();
expect(hasCompleted(RequestEntryState.Success)).toBeTrue();
expect(hasCompleted(RequestEntryState.ErrorStale)).toBeTrue();
expect(hasCompleted(RequestEntryState.SuccessStale)).toBeTrue();
expect(hasCompleted(RequestEntryState.RequestPending)).toBeFalse();
expect(hasCompleted(RequestEntryState.ResponsePending)).toBeFalse();
expect(hasCompleted(RequestEntryState.ResponsePendingStale)).toBeFalse();
});
});
describe(`isStale`, () => {
it(`should only return true if the given state is ResponsePendingStale, SuccessStale or ErrorStale`, () => {
expect(isStale(RequestEntryState.ResponsePendingStale)).toBeTrue();
expect(isStale(RequestEntryState.SuccessStale)).toBeTrue();
expect(isStale(RequestEntryState.ErrorStale)).toBeTrue();
expect(isStale(RequestEntryState.RequestPending)).toBeFalse();
expect(isStale(RequestEntryState.ResponsePending)).toBeFalse();
expect(isStale(RequestEntryState.Error)).toBeFalse();
expect(isStale(RequestEntryState.Success)).toBeFalse();
});
});

View File

@@ -3,8 +3,9 @@ export enum RequestEntryState {
ResponsePending = 'ResponsePending', ResponsePending = 'ResponsePending',
Error = 'Error', Error = 'Error',
Success = 'Success', Success = 'Success',
ResponsePendingStale = 'ResponsePendingStale',
ErrorStale = 'ErrorStale', ErrorStale = 'ErrorStale',
SuccessStale = 'SuccessStale' SuccessStale = 'SuccessStale',
} }
/** /**
@@ -42,12 +43,21 @@ export const isSuccessStale = (state: RequestEntryState) =>
*/ */
export const isResponsePending = (state: RequestEntryState) => export const isResponsePending = (state: RequestEntryState) =>
state === RequestEntryState.ResponsePending; state === RequestEntryState.ResponsePending;
/** /**
* Returns true if the given state is RequestPending or ResponsePending, * Returns true if the given state is ResponsePendingStale, false otherwise
* false otherwise */
export const isResponsePendingStale = (state: RequestEntryState) =>
state === RequestEntryState.ResponsePendingStale;
/**
* Returns true if the given state is RequestPending, RequestPendingStale, ResponsePending, or
* ResponsePendingStale, false otherwise
*/ */
export const isLoading = (state: RequestEntryState) => export const isLoading = (state: RequestEntryState) =>
isRequestPending(state) || isResponsePending(state); isRequestPending(state) ||
isResponsePending(state) ||
isResponsePendingStale(state);
/** /**
* If isLoading is true for the given state, this method returns undefined, we can't know yet. * If isLoading is true for the given state, this method returns undefined, we can't know yet.
@@ -82,7 +92,10 @@ export const hasCompleted = (state: RequestEntryState) =>
!isLoading(state); !isLoading(state);
/** /**
* Returns true if the given state is SuccessStale or ErrorStale, false otherwise * Returns true if the given state is isRequestPendingStale, isResponsePendingStale, SuccessStale or
* ErrorStale, false otherwise
*/ */
export const isStale = (state: RequestEntryState) => export const isStale = (state: RequestEntryState) =>
isSuccessStale(state) || isErrorStale(state); isResponsePendingStale(state) ||
isSuccessStale(state) ||
isErrorStale(state);

View File

@@ -48,9 +48,16 @@ describe('requestReducer', () => {
lastUpdated: 0 lastUpdated: 0
} }
}; };
const testResponsePendingState = {
[id1]: {
state: RequestEntryState.ResponsePending,
lastUpdated: 0
}
};
deepFreeze(testInitState); deepFreeze(testInitState);
deepFreeze(testSuccessState); deepFreeze(testSuccessState);
deepFreeze(testErrorState); deepFreeze(testErrorState);
deepFreeze(testResponsePendingState);
it('should return the current state when no valid actions have been made', () => { it('should return the current state when no valid actions have been made', () => {
const action = new NullAction(); const action = new NullAction();
@@ -91,23 +98,64 @@ describe('requestReducer', () => {
expect(newState[id1].response).toEqual(undefined); expect(newState[id1].response).toEqual(undefined);
}); });
it('should set state to Success for the given RestRequest in the state, in response to a SUCCESS action', () => { describe(`in response to a SUCCESS action`, () => {
const state = testInitState; let startState;
describe(`when the entry isn't stale`, () => {
beforeEach(() => {
startState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ResponsePending
})
});
deepFreeze(startState);
});
it('should set state to Success for the given RestRequest in the state', () => {
const action = new RequestSuccessAction(id1, 200); const action = new RequestSuccessAction(id1, 200);
const newState = requestReducer(state, action); const newState = requestReducer(startState, action);
expect(newState[id1].request.uuid).toEqual(id1); expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[id1].request.href).toEqual(link1); expect(newState[id1].request.href).toEqual(link1);
expect(newState[id1].state).toEqual(RequestEntryState.Success); expect(newState[id1].state).toEqual(RequestEntryState.Success);
expect(newState[id1].response.statusCode).toEqual(200); expect(newState[id1].response.statusCode).toEqual(200);
}); });
});
it('should set state to Error for the given RestRequest in the state, in response to an ERROR action', () => { describe(`when the entry is stale`, () => {
const state = testInitState; beforeEach(() => {
startState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ResponsePendingStale
})
});
deepFreeze(startState);
});
it('should set state to SuccessStale for the given RestRequest in the state', () => {
const action = new RequestSuccessAction(id1, 200);
const newState = requestReducer(startState, action);
expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[id1].request.href).toEqual(link1);
expect(newState[id1].state).toEqual(RequestEntryState.SuccessStale);
expect(newState[id1].response.statusCode).toEqual(200);
});
});
});
describe(`in response to an ERROR action`, () => {
let startState;
describe(`when the entry isn't stale`, () => {
beforeEach(() => {
startState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ResponsePending
})
});
deepFreeze(startState);
});
it('should set state to Error for the given RestRequest in the state', () => {
const action = new RequestErrorAction(id1, 404, 'Not Found'); const action = new RequestErrorAction(id1, 404, 'Not Found');
const newState = requestReducer(state, action); const newState = requestReducer(startState, action);
expect(newState[id1].request.uuid).toEqual(id1); expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[id1].request.href).toEqual(link1); expect(newState[id1].request.href).toEqual(link1);
@@ -115,6 +163,30 @@ describe('requestReducer', () => {
expect(newState[id1].response.statusCode).toEqual(404); expect(newState[id1].response.statusCode).toEqual(404);
expect(newState[id1].response.errorMessage).toEqual('Not Found'); expect(newState[id1].response.errorMessage).toEqual('Not Found');
}); });
});
describe(`when the entry is stale`, () => {
beforeEach(() => {
startState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ResponsePendingStale
})
});
deepFreeze(startState);
});
it('should set state to ErrorStale for the given RestRequest in the state', () => {
const action = new RequestErrorAction(id1, 404, 'Not Found');
const newState = requestReducer(startState, action);
expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[id1].request.href).toEqual(link1);
expect(newState[id1].state).toEqual(RequestEntryState.ErrorStale);
expect(newState[id1].response.statusCode).toEqual(404);
expect(newState[id1].response.errorMessage).toEqual('Not Found');
});
});
});
it('should update the response\'s timeCompleted for the given RestRequest in the state, in response to a RESET_TIMESTAMPS action', () => { it('should update the response\'s timeCompleted for the given RestRequest in the state, in response to a RESET_TIMESTAMPS action', () => {
const update = Object.assign({}, testInitState[id1], { const update = Object.assign({}, testInitState[id1], {
@@ -145,8 +217,92 @@ describe('requestReducer', () => {
expect(newState[id1]).toBeNull(); expect(newState[id1]).toBeNull();
}); });
describe(`in response to a STALE action`, () => {
describe(`when the entry has been removed`, () => {
it(`shouldn't do anything`, () => {
const startState = {
[id1]: null
};
deepFreeze(startState);
const action = new RequestStaleAction(id1);
const newState = requestReducer(startState, action);
expect(newState[id1]).toBeNull();
});
});
describe(`for stale entries`, () => {
it(`shouldn't do anything`, () => {
const rpsStartState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ResponsePendingStale
})
});
deepFreeze(rpsStartState);
const action = new RequestStaleAction(id1);
let newState = requestReducer(rpsStartState, action);
expect(newState[id1].state).toEqual(rpsStartState[id1].state);
expect(newState[id1].lastUpdated).toBe(rpsStartState[id1].lastUpdated);
const ssStartState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.SuccessStale
})
});
newState = requestReducer(ssStartState, action);
expect(newState[id1].state).toEqual(ssStartState[id1].state);
expect(newState[id1].lastUpdated).toBe(ssStartState[id1].lastUpdated);
const esStartState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.ErrorStale
})
});
newState = requestReducer(esStartState, action);
expect(newState[id1].state).toEqual(esStartState[id1].state);
expect(newState[id1].lastUpdated).toBe(esStartState[id1].lastUpdated);
});
});
describe(`for and entry with state: RequestPending`, () => {
it(`shouldn't do anything`, () => {
const startState = Object.assign({}, testInitState, {
[id1]: Object.assign({}, testInitState[id1], {
state: RequestEntryState.RequestPending
})
});
const action = new RequestStaleAction(id1);
const newState = requestReducer(startState, action);
expect(newState[id1].state).toEqual(startState[id1].state);
expect(newState[id1].lastUpdated).toBe(startState[id1].lastUpdated);
});
});
describe(`for an entry with state: ResponsePending`, () => {
it(`should set the state to ResponsePendingStale`, () => {
const state = testResponsePendingState;
const action = new RequestStaleAction(id1);
const newState = requestReducer(state, action);
expect(newState[id1].state).toEqual(RequestEntryState.ResponsePendingStale);
expect(newState[id1].lastUpdated).toBe(action.lastUpdated);
});
});
describe(`for an entry with state: Success`, () => { describe(`for an entry with state: Success`, () => {
it(`should set the state to SuccessStale, in response to a STALE action`, () => { it(`should set the state to SuccessStale`, () => {
const state = testSuccessState; const state = testSuccessState;
const action = new RequestStaleAction(id1); const action = new RequestStaleAction(id1);
@@ -158,7 +314,7 @@ describe('requestReducer', () => {
}); });
describe(`for an entry with state: Error`, () => { describe(`for an entry with state: Error`, () => {
it(`should set the state to ErrorStale, in response to a STALE action`, () => { it(`should set the state to ErrorStale`, () => {
const state = testErrorState; const state = testErrorState;
const action = new RequestStaleAction(id1); const action = new RequestStaleAction(id1);
@@ -168,5 +324,5 @@ describe('requestReducer', () => {
expect(newState[id1].lastUpdated).toBe(action.lastUpdated); expect(newState[id1].lastUpdated).toBe(action.lastUpdated);
}); });
}); });
});
}); });

View File

@@ -11,7 +11,13 @@ import {
ResetResponseTimestampsAction ResetResponseTimestampsAction
} from './request.actions'; } from './request.actions';
import { isNull } from '../../shared/empty.util'; import { isNull } from '../../shared/empty.util';
import { hasSucceeded, isStale, RequestEntryState } from './request-entry-state.model'; import {
hasSucceeded,
isStale,
RequestEntryState,
isRequestPending,
isResponsePending
} from './request-entry-state.model';
import { RequestState } from './request-state.model'; import { RequestState } from './request-state.model';
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
@@ -91,14 +97,17 @@ function executeRequest(storeState: RequestState, action: RequestExecuteAction):
* the new storeState, with the response added to the request * the new storeState, with the response added to the request
*/ */
function completeSuccessRequest(storeState: RequestState, action: RequestSuccessAction): RequestState { function completeSuccessRequest(storeState: RequestState, action: RequestSuccessAction): RequestState {
if (isNull(storeState[action.payload.uuid])) { const prevEntry = storeState[action.payload.uuid];
if (isNull(prevEntry)) {
// after a request has been removed it's possible pending changes still come in. // after a request has been removed it's possible pending changes still come in.
// Don't store them // Don't store them
return storeState; return storeState;
} else { } else {
return Object.assign({}, storeState, { return Object.assign({}, storeState, {
[action.payload.uuid]: Object.assign({}, storeState[action.payload.uuid], { [action.payload.uuid]: Object.assign({}, prevEntry, {
state: RequestEntryState.Success, // If a response comes in for a request that's already stale, still store it otherwise
// components that are waiting for it might freeze
state: isStale(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.Success,
response: { response: {
timeCompleted: action.payload.timeCompleted, timeCompleted: action.payload.timeCompleted,
lastUpdated: action.payload.timeCompleted, lastUpdated: action.payload.timeCompleted,
@@ -124,14 +133,17 @@ function completeSuccessRequest(storeState: RequestState, action: RequestSuccess
* the new storeState, with the response added to the request * the new storeState, with the response added to the request
*/ */
function completeFailedRequest(storeState: RequestState, action: RequestErrorAction): RequestState { function completeFailedRequest(storeState: RequestState, action: RequestErrorAction): RequestState {
if (isNull(storeState[action.payload.uuid])) { const prevEntry = storeState[action.payload.uuid];
if (isNull(prevEntry)) {
// after a request has been removed it's possible pending changes still come in. // after a request has been removed it's possible pending changes still come in.
// Don't store them // Don't store them
return storeState; return storeState;
} else { } else {
return Object.assign({}, storeState, { return Object.assign({}, storeState, {
[action.payload.uuid]: Object.assign({}, storeState[action.payload.uuid], { [action.payload.uuid]: Object.assign({}, prevEntry, {
state: RequestEntryState.Error, // If a response comes in for a request that's already stale, still store it otherwise
// components that are waiting for it might freeze
state: isStale(prevEntry.state) ? RequestEntryState.ErrorStale : RequestEntryState.Error,
response: { response: {
timeCompleted: action.payload.timeCompleted, timeCompleted: action.payload.timeCompleted,
lastUpdated: action.payload.timeCompleted, lastUpdated: action.payload.timeCompleted,
@@ -155,24 +167,29 @@ function completeFailedRequest(storeState: RequestState, action: RequestErrorAct
* the new storeState, set to stale * the new storeState, set to stale
*/ */
function expireRequest(storeState: RequestState, action: RequestStaleAction): RequestState { function expireRequest(storeState: RequestState, action: RequestStaleAction): RequestState {
if (isNull(storeState[action.payload.uuid])) {
// after a request has been removed it's possible pending changes still come in.
// Don't store them
return storeState;
} else {
const prevEntry = storeState[action.payload.uuid]; const prevEntry = storeState[action.payload.uuid];
if (isStale(prevEntry.state)) { if (isNull(prevEntry) || isStale(prevEntry.state) || isRequestPending(prevEntry.state)) {
// No need to do anything if the entry doesn't exist, is already stale, or if the request is
// still pending, because that means it still needs to be sent to the server. Any response
// is guaranteed to have been generated after the request was set to stale.
return storeState; return storeState;
} else { } else {
let nextRequestEntryState: RequestEntryState;
if (isResponsePending(prevEntry.state)) {
nextRequestEntryState = RequestEntryState.ResponsePendingStale;
} else if (hasSucceeded(prevEntry.state)) {
nextRequestEntryState = RequestEntryState.SuccessStale;
} else {
nextRequestEntryState = RequestEntryState.ErrorStale;
}
return Object.assign({}, storeState, { return Object.assign({}, storeState, {
[action.payload.uuid]: Object.assign({}, prevEntry, { [action.payload.uuid]: Object.assign({}, prevEntry, {
state: hasSucceeded(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.ErrorStale, state: nextRequestEntryState,
lastUpdated: action.lastUpdated lastUpdated: action.lastUpdated
}) })
}); });
} }
} }
}
/** /**
* Reset the timeCompleted property of all responses * Reset the timeCompleted property of all responses

View File

@@ -164,7 +164,7 @@ export class RequestService {
this.getByHref(request.href).pipe( this.getByHref(request.href).pipe(
take(1)) take(1))
.subscribe((re: RequestEntry) => { .subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && isLoading(re.state)); isPending = (hasValue(re) && isLoading(re.state) && !isStale(re.state));
}); });
return isPending; return isPending;
} }

View File

@@ -9,7 +9,7 @@ import SpyObj = jasmine.SpyObj;
describe('ServerCheckGuard', () => { describe('ServerCheckGuard', () => {
let guard: ServerCheckGuard; let guard: ServerCheckGuard;
let router: Router; let router: Router;
const eventSubject = new ReplaySubject<RouterEvent>(1); let eventSubject: ReplaySubject<RouterEvent>;
let rootDataServiceStub: SpyObj<RootDataService>; let rootDataServiceStub: SpyObj<RootDataService>;
let testScheduler: TestScheduler; let testScheduler: TestScheduler;
let redirectUrlTree: UrlTree; let redirectUrlTree: UrlTree;
@@ -24,6 +24,7 @@ describe('ServerCheckGuard', () => {
findRoot: jasmine.createSpy('findRoot') findRoot: jasmine.createSpy('findRoot')
}); });
redirectUrlTree = new UrlTree(); redirectUrlTree = new UrlTree();
eventSubject = new ReplaySubject<RouterEvent>(1);
router = { router = {
events: eventSubject.asObservable(), events: eventSubject.asObservable(),
navigateByUrl: jasmine.createSpy('navigateByUrl'), navigateByUrl: jasmine.createSpy('navigateByUrl'),
@@ -64,10 +65,10 @@ describe('ServerCheckGuard', () => {
}); });
describe(`listenForRouteChanges`, () => { describe(`listenForRouteChanges`, () => {
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => { it(`should invalidate the root cache, when the method is first called`, () => {
testScheduler.run(() => { testScheduler.run(() => {
guard.listenForRouteChanges(); guard.listenForRouteChanges();
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false); expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(1);
}); });
}); });
@@ -80,7 +81,8 @@ describe('ServerCheckGuard', () => {
eventSubject.next(new NavigationEnd(2,'', '')); eventSubject.next(new NavigationEnd(2,'', ''));
eventSubject.next(new NavigationStart(3,'')); eventSubject.next(new NavigationStart(3,''));
}); });
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3); // once when the method is first called, and then 3 times for NavigationStart events
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(1 + 3);
}); });
}); });
}); });

View File

@@ -53,10 +53,8 @@ export class ServerCheckGuard implements CanActivateChild {
*/ */
listenForRouteChanges(): void { listenForRouteChanges(): void {
// we'll always be too late for the first NavigationStart event with the router subscribe below, // we'll always be too late for the first NavigationStart event with the router subscribe below,
// so this statement is for the very first route operation. A `find` without using the cache, // so this statement is for the very first route operation.
// rather than an invalidateRootCache, because invalidating as the app is bootstrapping can this.rootDataService.invalidateRootCache();
// break other features
this.rootDataService.findRoot(false);
this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof NavigationStart), filter(event => event instanceof NavigationStart),

View File

@@ -1,4 +1,3 @@
import { cold, hot } from 'jasmine-marbles';
import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service'; import { HALEndpointService } from './hal-endpoint.service';
@@ -7,12 +6,17 @@ import { combineLatest as observableCombineLatest, of as observableOf } from 'rx
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { TestScheduler } from 'rxjs/testing';
import { RemoteData } from '../data/remote-data';
import { RequestEntryState } from '../data/request-entry-state.model';
describe('HALEndpointService', () => { describe('HALEndpointService', () => {
let service: HALEndpointService; let service: HALEndpointService;
let requestService: RequestService; let requestService: RequestService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let envConfig; let envConfig;
let testScheduler;
let remoteDataMocks;
const endpointMap = { const endpointMap = {
test: { test: {
href: 'https://rest.api/test' href: 'https://rest.api/test'
@@ -68,7 +72,30 @@ describe('HALEndpointService', () => {
}; };
const linkPath = 'test'; const linkPath = 'test';
const timeStamp = new Date().getTime();
const msToLive = 15 * 60 * 1000;
const payload = {
_links: endpointMaps[one]
};
const statusCodeSuccess = 200;
const statusCodeError = 404;
const errorMessage = 'not found';
remoteDataMocks = {
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined),
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess),
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess),
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
};
beforeEach(() => { beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
// asserting the two objects are equal
// e.g. using chai.
expect(actual).toEqual(expected);
});
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildFromHref: createSuccessfulRemoteDataObject$({ buildFromHref: createSuccessfulRemoteDataObject$({
@@ -111,20 +138,28 @@ describe('HALEndpointService', () => {
}); });
it(`should return the endpoint URL for the service's linkPath`, () => { it(`should return the endpoint URL for the service's linkPath`, () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(service as any, 'getEndpointAt').and spyOn(service as any, 'getEndpointAt').and
.returnValue(hot('a-', { a: 'https://rest.api/test' })); .returnValue(cold('a-', { a: 'https://rest.api/test' }));
const result = service.getEndpoint(linkPath); const result = service.getEndpoint(linkPath);
const expected = cold('(b|)', { b: endpointMap.test.href }); const expected = '(b|)';
expect(result).toBeObservable(expected); const values = {
b: endpointMap.test.href
};
expectObservable(result).toBe(expected, values);
});
}); });
it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { it('should return undefined for a linkPath that isn\'t in the endpoint map', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(service as any, 'getEndpointAt').and spyOn(service as any, 'getEndpointAt').and
.returnValue(hot('a-', { a: undefined })); .returnValue(cold('a-', { a: undefined }));
const result = service.getEndpoint('unknown'); const result = service.getEndpoint('unknown');
const expected = cold('(b|)', { b: undefined }); const expected = '(b|)';
expect(result).toBeObservable(expected); const values = { b: undefined };
expectObservable(result).toBe(expected, values);
});
}); });
}); });
@@ -183,29 +218,118 @@ describe('HALEndpointService', () => {
}); });
it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => { it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(service as any, 'getRootEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('----')); .returnValue(cold('----'));
const result = service.isEnabledOnRestApi(linkPath); const result = service.isEnabledOnRestApi(linkPath);
const expected = cold('b---', { b: undefined }); const expected = 'b---';
expect(result).toBeObservable(expected); const values = { b: undefined };
expectObservable(result).toBe(expected, values);
});
}); });
it('should return true if the service\'s linkPath is in the endpoint map', () => { it('should return true if the service\'s linkPath is in the endpoint map', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(service as any, 'getRootEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('--a-', { a: endpointMap })); .returnValue(cold('--a-', { a: endpointMap }));
const result = service.isEnabledOnRestApi(linkPath); const result = service.isEnabledOnRestApi(linkPath);
const expected = cold('b-c-', { b: undefined, c: true }); const expected = 'b-c-';
expect(result).toBeObservable(expected); const values = { b: undefined, c: true };
expectObservable(result).toBe(expected, values);
});
}); });
it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => { it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(service as any, 'getRootEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('--a-', { a: endpointMap })); .returnValue(cold('--a-', { a: endpointMap }));
const result = service.isEnabledOnRestApi('unknown'); const result = service.isEnabledOnRestApi('unknown');
const expected = cold('b-c-', { b: undefined, c: false }); const expected = 'b-c-';
expect(result).toBeObservable(expected); const values = { b: undefined, c: false };
expectObservable(result).toBe(expected, values);
});
});
});
describe(`getEndpointMapAt`, () => {
const href = 'https://rest.api/some/sub/path';
it(`should call requestService.send with a new EndpointMapRequest for the given href. useCachedVersionIfAvailable should be true`, () => {
testScheduler.run(() => {
(service as any).getEndpointMapAt(href);
});
const expected = new EndpointMapRequest(requestService.generateRequestId(), href);
expect(requestService.send).toHaveBeenCalledWith(expected, true);
});
it(`should call rdbService.buildFromHref with the given href`, () => {
testScheduler.run(() => {
(service as any).getEndpointMapAt(href);
});
expect(rdbService.buildFromHref).toHaveBeenCalledWith(href);
});
describe(`when the RemoteData returned from rdbService is stale`, () => {
it(`should re-request it`, () => {
spyOn(service as any, 'getEndpointMapAt').and.callThrough();
testScheduler.run(({ cold }) => {
(rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', { a: remoteDataMocks.ResponsePendingStale }));
// we need to subscribe to the result, to ensure the "tap" that does the re-request can fire
(service as any).getEndpointMapAt(href).subscribe();
});
expect((service as any).getEndpointMapAt).toHaveBeenCalledTimes(2);
});
});
describe(`when the RemoteData returned from rdbService isn't stale`, () => {
it(`should not re-request it`, () => {
spyOn(service as any, 'getEndpointMapAt').and.callThrough();
testScheduler.run(({ cold }) => {
(rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', { a: remoteDataMocks.ResponsePending }));
// we need to subscribe to the result, to ensure the "tap" that does the re-request can fire
(service as any).getEndpointMapAt(href).subscribe();
});
expect((service as any).getEndpointMapAt).toHaveBeenCalledTimes(1);
});
});
it(`should emit exactly once, returning the endpoint map in the response, when the RemoteData completes`, () => {
testScheduler.run(({ cold, expectObservable }) => {
(rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a-b-c-d-e-f-g-h-i-j-k-l', {
a: remoteDataMocks.RequestPending,
b: remoteDataMocks.ResponsePending,
c: remoteDataMocks.ResponsePendingStale,
d: remoteDataMocks.SuccessStale,
e: remoteDataMocks.RequestPending,
f: remoteDataMocks.ResponsePending,
g: remoteDataMocks.Success,
h: remoteDataMocks.SuccessStale,
i: remoteDataMocks.RequestPending,
k: remoteDataMocks.ResponsePending,
l: remoteDataMocks.Error,
}));
const expected = '------------(g|)';
const values = {
g: endpointMaps[one]
};
expectObservable((service as any).getEndpointMapAt(one)).toBe(expected, values);
});
});
it(`should emit undefined when the response doesn't have a payload`, () => {
testScheduler.run(({ cold, expectObservable }) => {
(rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', {
a: remoteDataMocks.Error,
}));
const expected = '(a|)';
const values = {
g: undefined
};
expectObservable((service as any).getEndpointMapAt(href)).toBe(expected, values);
});
}); });
}); });

View File

@@ -1,5 +1,12 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith, switchMap, take } from 'rxjs/operators'; import {
distinctUntilChanged,
map,
startWith,
switchMap,
take,
tap, filter
} from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { EndpointMapRequest } from '../data/request.models'; import { EndpointMapRequest } from '../data/request.models';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
@@ -9,7 +16,7 @@ import { EndpointMap } from '../cache/response.models';
import { getFirstCompletedRemoteData } from './operators'; import { getFirstCompletedRemoteData } from './operators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { UnCacheableObject } from './uncacheable-object.model'; import { CacheableObject } from '../cache/cacheable-object.model';
@Injectable() @Injectable()
export class HALEndpointService { export class HALEndpointService {
@@ -33,9 +40,18 @@ export class HALEndpointService {
this.requestService.send(request, true); this.requestService.send(request, true);
return this.rdbService.buildFromHref<UnCacheableObject>(href).pipe( return this.rdbService.buildFromHref<CacheableObject>(href).pipe(
// Re-request stale responses
tap((rd: RemoteData<CacheableObject>) => {
if (hasValue(rd) && rd.isStale) {
this.getEndpointMapAt(href);
}
}),
// Filter out all stale responses. We're only interested in a single, non-stale,
// completed RemoteData
filter((rd: RemoteData<CacheableObject>) => !rd.isStale),
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((response: RemoteData<UnCacheableObject>) => { map((response: RemoteData<CacheableObject>) => {
if (hasValue(response.payload)) { if (hasValue(response.payload)) {
return response.payload._links; return response.payload._links;
} else { } else {