intermediate commit

This commit is contained in:
lotte
2018-10-17 13:18:01 +02:00
parent b1c6d68cc5
commit 2330e96158
75 changed files with 387 additions and 1067 deletions

View File

@@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut] animations: [fadeInOut]
}) })
export class TopLevelCommunityListComponent { export class TopLevelCommunityListComponent {
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>; communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
config: PaginationComponentOptions; config: PaginationComponentOptions;

View File

@@ -8,21 +8,18 @@ import { SearchService } from './search.service';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { RouterStub } from '../../shared/testing/router-stub'; import { RouterStub } from '../../shared/testing/router-stub';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { RequestEntry } from '../../core/data/request.reducer'; import { RequestEntry } from '../../core/data/request.reducer';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { import {
FacetConfigSuccessResponse, FacetConfigSuccessResponse,
SearchSuccessResponse SearchSuccessResponse
} from '../../core/cache/response-cache.models'; } from '../../core/cache/response.models';
import { SearchQueryResponse } from './search-query-response.model'; import { SearchQueryResponse } from './search-query-response.model';
import { SearchFilterConfig } from './search-filter-config.model'; import { SearchFilterConfig } from './search-filter-config.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
@@ -54,7 +51,6 @@ describe('SearchService', () => {
providers: [ providers: [
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} }, { provide: HALEndpointService, useValue: {} },
@@ -86,9 +82,8 @@ describe('SearchService', () => {
}; };
const remoteDataBuildService = { const remoteDataBuildService = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => { toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
return observableCombineLatest(requestEntryObs, return observableCombineLatest(requestEntryObs, payloadObs).pipe(
responseCacheObs, payloadObs).pipe(
map(([req, res, pay]) => { map(([req, res, pay]) => {
return { req, res, pay }; return { req, res, pay };
}) })
@@ -113,7 +108,6 @@ describe('SearchService', () => {
providers: [ providers: [
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: route }, { provide: ActivatedRoute, useValue: route },
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService }, { provide: HALEndpointService, useValue: halService },
@@ -162,10 +156,8 @@ describe('SearchService', () => {
const searchOptions = new PaginatedSearchOptions({}); const searchOptions = new PaginatedSearchOptions({});
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] }); const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
const response = new SearchSuccessResponse(queryResponse, '200'); const response = new SearchSuccessResponse(queryResponse, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.search(searchOptions).subscribe((t) => { searchService.search(searchOptions).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called
@@ -192,10 +184,8 @@ describe('SearchService', () => {
const endPoint = 'http://endpoint.com/test/config'; const endPoint = 'http://endpoint.com/test/config';
const filterConfig = [new SearchFilterConfig()]; const filterConfig = [new SearchFilterConfig()];
const response = new FacetConfigSuccessResponse(filterConfig, '200'); const response = new FacetConfigSuccessResponse(filterConfig, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.getConfig(null).subscribe((t) => { searchService.getConfig(null).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called
@@ -224,10 +214,8 @@ describe('SearchService', () => {
const requestUrl = endPoint + '?scope=' + scope; const requestUrl = endPoint + '?scope=' + scope;
const filterConfig = [new SearchFilterConfig()]; const filterConfig = [new SearchFilterConfig()];
const response = new FacetConfigSuccessResponse(filterConfig, '200'); const response = new FacetConfigSuccessResponse(filterConfig, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.getConfig(scope).subscribe((t) => { searchService.getConfig(scope).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called

View File

@@ -1,4 +1,4 @@
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { import {
ActivatedRoute, ActivatedRoute,
@@ -7,15 +7,13 @@ import {
Router, Router,
UrlSegmentGroup UrlSegmentGroup
} from '@angular/router'; } from '@angular/router';
import { flatMap, map, switchMap } from 'rxjs/operators'; import { filter, flatMap, map, switchMap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { import {
FacetConfigSuccessResponse, FacetConfigSuccessResponse,
FacetValueSuccessResponse, FacetValueSuccessResponse,
SearchSuccessResponse SearchSuccessResponse
} from '../../core/cache/response-cache.models'; } from '../../core/cache/response.models';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ResponseParsingService } from '../../core/data/parsing.service'; import { ResponseParsingService } from '../../core/data/parsing.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
@@ -24,7 +22,11 @@ import { RequestService } from '../../core/data/request.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { configureRequest, getSucceededRemoteData } from '../../core/shared/operators'; import {
configureRequest,
getResponseFromEntry,
getSucceededRemoteData
} from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedSearchResult } from '../normalized-search-result.model'; import { NormalizedSearchResult } from '../normalized-search-result.model';
@@ -45,6 +47,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { ResourceType } from '../../core/shared/resource-type'; import { ResourceType } from '../../core/shared/resource-type';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { RequestEntry } from '../../core/data/request.reducer';
/** /**
* Service that performs all general actions that have to do with the search page * Service that performs all general actions that have to do with the search page
@@ -68,7 +71,6 @@ export class SearchService implements OnDestroy {
constructor(private router: Router, constructor(private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
private rdb: RemoteDataBuildService, private rdb: RemoteDataBuildService,
private halService: HALEndpointService, private halService: HALEndpointService,
@@ -101,13 +103,9 @@ export class SearchService implements OnDestroy {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
);
// get search results from response cache // get search results from response cache
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe( const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: SearchSuccessResponse) => response.results) map((response: SearchSuccessResponse) => response.results)
); );
@@ -139,8 +137,8 @@ export class SearchService implements OnDestroy {
}) })
); );
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.pageInfo) map((response: FacetValueSuccessResponse) => response.pageInfo)
); );
@@ -150,7 +148,7 @@ export class SearchService implements OnDestroy {
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
/** /**
@@ -185,18 +183,14 @@ export class SearchService implements OnDestroy {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
);
// get search results from response cache // get search results from response cache
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe( const facetConfigObs: Observable<SearchFilterConfig[]> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: FacetConfigSuccessResponse) => map((response: FacetConfigSuccessResponse) =>
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result))) response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); return this.rdb.toRemoteDataObservable(requestEntryObs, facetConfigObs);
} }
/** /**
@@ -232,18 +226,14 @@ export class SearchService implements OnDestroy {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
);
// get search results from response cache // get search results from response cache
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe( const facetValueObs: Observable<FacetValue[]> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.results) map((response: FacetValueSuccessResponse) => response.results)
); );
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.pageInfo) map((response: FacetValueSuccessResponse) => response.pageInfo)
); );
@@ -253,7 +243,7 @@ export class SearchService implements OnDestroy {
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
/** /**

View File

@@ -2,15 +2,15 @@ import { Observable, of as observableOf, throwError as observableThrowError } fr
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models'; import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
import { AuthStatusResponse, ErrorResponse } from '../cache/response-cache.models';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
@Injectable() @Injectable()
export class AuthRequestService { export class AuthRequestService {
@@ -19,18 +19,17 @@ export class AuthRequestService {
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService) { protected requestService: RequestService) {
} }
protected fetchRequest(request: RestRequest): Observable<any> { protected fetchRequest(request: RestRequest): Observable<any> {
return this.responseCache.get(request.href).pipe( return this.requestService.getByHref(request.href).pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
tap(() => this.responseCache.remove(request.href)), // tap(() => this.responseCache.remove(request.href)),
mergeMap((response) => { mergeMap((response) => {
if (response.isSuccessful && isNotEmpty(response)) { if (response.isSuccessful && isNotEmpty(response)) {
return observableOf((response as AuthStatusResponse).response); return observableOf((response as AuthStatusResponse).response);
} else if (!response.isSuccessful) { } else if (!response.isSuccessful) {
return observableThrowError(new Error((response as ErrorResponse).errorMessage)); return observableThrowError(new Error((response as ErrorResponse).errorMessage));
} }

View File

@@ -1,4 +1,4 @@
import { AuthStatusResponse } from '../cache/response-cache.models'; import { AuthStatusResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
import { AuthObjectFactory } from './auth-object-factory'; import { AuthObjectFactory } from './auth-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models'; import { AuthStatusResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -2,10 +2,8 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
@@ -14,7 +12,6 @@ import { BrowseService } from './browse.service';
describe('BrowseService', () => { describe('BrowseService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: BrowseService; let service: BrowseService;
let responseCache: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
@@ -79,22 +76,10 @@ describe('BrowseService', () => {
}) })
]; ];
function initMockResponseCacheService(isSuccessful: boolean) {
const rcs = getMockResponseCacheService();
(rcs.get as any).and.returnValue(cold('b-', {
b: {
response: {
isSuccessful,
payload: browseDefinitions,
}
}
}));
return rcs;
}
function initTestService() { function initTestService() {
return new BrowseService( return new BrowseService(
responseCache,
requestService, requestService,
halService, halService,
rdbService rdbService
@@ -108,7 +93,6 @@ describe('BrowseService', () => {
describe('getBrowseDefinitions', () => { describe('getBrowseDefinitions', () => {
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
@@ -147,7 +131,6 @@ describe('BrowseService', () => {
const mockAuthorName = 'Donald Smith'; const mockAuthorName = 'Donald Smith';
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
@@ -221,7 +204,6 @@ describe('BrowseService', () => {
describe('if getBrowseDefinitions fires', () => { describe('if getBrowseDefinitions fires', () => {
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();
@@ -277,7 +259,6 @@ describe('BrowseService', () => {
describe('if getBrowseDefinitions doesn\'t fire', () => { describe('if getBrowseDefinitions doesn\'t fire', () => {
it('should return undefined', () => { it('should return undefined', () => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
service = initTestService(); service = initTestService();

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
import { import {
ensureArrayHasValue, ensureArrayHasValue, hasValue,
hasValueOperator, hasValueOperator,
isEmpty, isEmpty,
isNotEmpty, isNotEmpty,
@@ -11,16 +11,13 @@ import {
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SortOptions } from '../cache/models/sort-options.model'; import { SortOptions } from '../cache/models/sort-options.model';
import { GenericSuccessResponse } from '../cache/response-cache.models'; import { GenericSuccessResponse } from '../cache/response.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { import {
BrowseEndpointRequest, BrowseEndpointRequest,
BrowseEntriesRequest, BrowseEntriesRequest,
BrowseItemsRequest, BrowseItemsRequest,
GetRequest,
RestRequest RestRequest
} from '../data/request.models'; } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
@@ -29,14 +26,15 @@ import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { import {
configureRequest, configureRequest,
filterSuccessfulResponses, getBrowseDefinitionLinks, filterSuccessfulResponses,
getBrowseDefinitionLinks,
getRemoteDataPayload, getRemoteDataPayload,
getRequestFromSelflink, getRequestFromSelflink
getResponseFromSelflink
} from '../shared/operators'; } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { RequestEntry } from '../data/request.reducer';
@Injectable() @Injectable()
export class BrowseService { export class BrowseService {
@@ -56,7 +54,6 @@ export class BrowseService {
} }
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
private rdb: RemoteDataBuildService, private rdb: RemoteDataBuildService,
@@ -73,10 +70,8 @@ export class BrowseService {
const href$ = request$.pipe(map((request: RestRequest) => request.href)); const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); const payload$ = requestEntry$.pipe(
const payload$ = responseCache$.pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload), map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(), ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions map((definitions: BrowseDefinition[]) => definitions
@@ -84,7 +79,7 @@ export class BrowseService {
distinctUntilChanged() distinctUntilChanged()
); );
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
} }
getBrowseEntriesFor(definitionID: string, options: { getBrowseEntriesFor(definitionID: string, options: {
@@ -118,11 +113,9 @@ export class BrowseService {
const href$ = request$.pipe(map((request: RestRequest) => request.href)); const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe( const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)), map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, { map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
@@ -130,7 +123,7 @@ export class BrowseService {
distinctUntilChanged() distinctUntilChanged()
); );
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
} }
/** /**
@@ -175,11 +168,9 @@ export class BrowseService {
const href$ = request$.pipe(map((request: RestRequest) => request.href)); const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe( const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)), map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<Item>) => Object.assign(list, { map((list: PaginatedList<Item>) => Object.assign(list, {
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
@@ -187,7 +178,7 @@ export class BrowseService {
distinctUntilChanged() distinctUntilChanged()
); );
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
} }
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> { getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {

View File

@@ -1,11 +1,11 @@
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
of as observableOf,
Observable, Observable,
of as observableOf,
race as observableRace race as observableRace
} from 'rxjs'; } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators'; import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
@@ -16,22 +16,18 @@ import { RequestService } from '../../data/request.service';
import { NormalizedObject } from '../models/normalized-object.model'; import { NormalizedObject } from '../models/normalized-object.model';
import { ObjectCacheService } from '../object-cache.service'; import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models'; import { DSOSuccessResponse, ErrorResponse } from '../response.models';
import { ResponseCacheEntry } from '../response-cache.reducer';
import { ResponseCacheService } from '../response-cache.service';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { PageInfo } from '../../shared/page-info.model'; import { PageInfo } from '../../shared/page-info.model';
import { import {
filterSuccessfulResponses,
getRequestFromSelflink, getRequestFromSelflink,
getResourceLinksFromResponse, getResourceLinksFromResponse
getResponseFromSelflink,
filterSuccessfulResponses
} from '../../shared/operators'; } from '../../shared/operators';
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
constructor(protected objectCache: ObjectCacheService, constructor(protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService) { protected requestService: RequestService) {
} }
@@ -39,19 +35,16 @@ export class RemoteDataBuildService {
if (typeof href$ === 'string') { if (typeof href$ === 'string') {
href$ = observableOf(href$); href$ = observableOf(href$);
} }
const requestHref$ = href$.pipe(flatMap((href: string) => const requestHref$ = href$.pipe(
this.objectCache.getRequestHrefBySelfLink(href))); switchMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href)),
);
const requestEntry$ = observableRace( const requestEntry$ = observableRace(
href$.pipe(getRequestFromSelflink(this.requestService)), href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService)) requestHref$.pipe(getRequestFromSelflink(this.requestService))
); );
const responseCache$ = observableRace(
href$.pipe(getResponseFromSelflink(this.responseCache)),
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
);
// always use self link if that is cached, only if it isn't, get it via the response. // always use self link if that is cached, only if it isn't, get it via the response.
const payload$ = const payload$ =
observableCombineLatest( observableCombineLatest(
@@ -59,7 +52,7 @@ export class RemoteDataBuildService {
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)), flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined) startWith(undefined)
), ),
responseCache$.pipe( requestEntry$.pipe(
getResourceLinksFromResponse(), getResourceLinksFromResponse(),
flatMap((resourceSelfLinks: string[]) => { flatMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceSelfLinks)) { if (isNotEmpty(resourceSelfLinks)) {
@@ -86,21 +79,21 @@ export class RemoteDataBuildService {
startWith(undefined), startWith(undefined),
distinctUntilChanged() distinctUntilChanged()
); );
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.toRemoteDataObservable(requestEntry$, payload$);
} }
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) { toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe( return observableCombineLatest(requestEntry$, requestEntry$.pipe(startWith(undefined)), payload$).pipe(
map(([reqEntry, resEntry, payload]) => { map(([reqEntry, payload]) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
let isSuccessful: boolean; let isSuccessful: boolean;
let error: RemoteDataError; let error: RemoteDataError;
if (hasValue(resEntry) && hasValue(resEntry.response)) { if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
isSuccessful = resEntry.response.isSuccessful; isSuccessful = reqEntry.response.isSuccessful;
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
if (hasValue(errorMessage)) { if (hasValue(errorMessage)) {
error = new RemoteDataError(resEntry.response.statusCode, errorMessage); error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
} }
} }
return new RemoteData( return new RemoteData(
@@ -120,9 +113,7 @@ export class RemoteDataBuildService {
} }
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); const tDomainList$ = requestEntry$.pipe(
const tDomainList$ = responseCache$.pipe(
getResourceLinksFromResponse(), getResourceLinksFromResponse(),
flatMap((resourceUUIDs: string[]) => { flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs).pipe( return this.objectCache.getList(resourceUUIDs).pipe(
@@ -135,12 +126,12 @@ export class RemoteDataBuildService {
startWith([]), startWith([]),
distinctUntilChanged() distinctUntilChanged()
); );
// tDomainList$.subscribe((t) => {console.log('domainlist', t)});
const pageInfo$ = responseCache$.pipe( const pageInfo$ = requestEntry$.pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => { map((response: DSOSuccessResponse) => {
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) { if (hasValue((response as DSOSuccessResponse).pageInfo)) {
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo; const resPageInfo = (response as DSOSuccessResponse).pageInfo;
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
} else { } else {
@@ -156,7 +147,7 @@ export class RemoteDataBuildService {
}) })
); );
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.toRemoteDataObservable(requestEntry$, payload$);
} }
build<TNormalized, TDomain>(normalized: TNormalized): TDomain { build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
@@ -204,8 +195,9 @@ export class RemoteDataBuildService {
} }
} }
}); });
const domainModel = getMapsTo(normalized.constructor); const domainModel = getMapsTo(normalized.constructor);
// console.log('domain model', normalized);
return Object.assign(new domainModel(), normalized, links); return Object.assign(new domainModel(), normalized, links);
} }

View File

@@ -1,72 +0,0 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { RestResponse } from './response-cache.models';
/**
* The list of ResponseCacheAction type definitions
*/
export const ResponseCacheActionTypes = {
ADD: type('dspace/core/cache/response/ADD'),
REMOVE: type('dspace/core/cache/response/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
};
/* tslint:disable:max-classes-per-file */
export class ResponseCacheAddAction implements Action {
type = ResponseCacheActionTypes.ADD;
payload: {
key: string,
response: RestResponse
timeAdded: number;
msToLive: number;
};
constructor(key: string, response: RestResponse, timeAdded: number, msToLive: number) {
this.payload = { key, response, timeAdded, msToLive };
}
}
/**
* An ngrx action to remove a request from the cache
*/
export class ResponseCacheRemoveAction implements Action {
type = ResponseCacheActionTypes.REMOVE;
payload: string;
/**
* Create a new ResponseCacheRemoveAction
* @param key
* The key of the request to remove
*/
constructor(key: string) {
this.payload = key;
}
}
/**
* An ngrx action to reset the timeAdded property of all cached objects
*/
export class ResetResponseCacheTimestampsAction implements Action {
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
payload: number;
/**
* Create a new ResetObjectCacheTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) {
this.payload = newTimestamp;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all ResponseCacheActions
*/
export type ResponseCacheAction
= ResponseCacheAddAction
| ResponseCacheRemoveAction
| ResetResponseCacheTimestampsAction;

View File

@@ -1,38 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles';
import { StoreActionTypes } from '../../store.actions';
import { ResponseCacheEffects } from './response-cache.effects';
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
describe('ResponseCacheEffects', () => {
let cacheEffects: ResponseCacheEffects;
let actions: Observable<any>;
const timestamp = 10000;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ResponseCacheEffects,
provideMockActions(() => actions),
// other providers
],
});
cacheEffects = TestBed.get(ResponseCacheEffects);
});
describe('fixTimestampsOnRehydrate$', () => {
it('should return a RESET_TIMESTAMPS action in response to a REHYDRATE action', () => {
spyOn(Date.prototype, 'getTime').and.callFake(() => {
return timestamp;
});
actions = hot('--a-', { a: { type: StoreActionTypes.REHYDRATE, payload: {} } });
const expected = cold('--b-', { b: new ResetResponseCacheTimestampsAction(new Date().getTime()) });
expect(cacheEffects.fixTimestampsOnRehydrate).toBeObservable(expected);
});
});
});

View File

@@ -1,27 +0,0 @@
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
import { StoreActionTypes } from '../../store.actions';
@Injectable()
export class ResponseCacheEffects {
/**
* When the store is rehydrated in the browser, set all cache
* timestamps to 'now', because the time zone of the server can
* differ from the client.
*
* This assumes that the server cached everything a negligible
* time ago, and will likely need to be revisited later
*/
@Effect() fixTimestampsOnRehydrate = this.actions$
.pipe(ofType(StoreActionTypes.REHYDRATE),
map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()))
);
constructor(private actions$: Actions,) {
}
}

View File

@@ -1,124 +0,0 @@
import * as deepFreeze from 'deep-freeze';
import { responseCacheReducer, ResponseCacheState } from './response-cache.reducer';
import {
ResponseCacheRemoveAction,
ResetResponseCacheTimestampsAction, ResponseCacheAddAction
} from './response-cache.actions';
import { RestResponse } from './response-cache.models';
class NullAction extends ResponseCacheRemoveAction {
type = null;
payload = null;
constructor() {
super(null);
}
}
describe('responseCacheReducer', () => {
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
const msToLive = 900000;
const uuids = [
'9e32a2e2-6b91-4236-a361-995ccdc14c60',
'598ce822-c357-46f3-ab70-63724d02d6ad',
'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
];
const testState: ResponseCacheState = {
[keys[0]]: {
key: keys[0],
response: new RestResponse(true, '200'),
timeAdded: new Date().getTime(),
msToLive: msToLive
},
[keys[1]]: {
key: keys[1],
response: new RestResponse(true, '200'),
timeAdded: new Date().getTime(),
msToLive: msToLive
}
};
deepFreeze(testState);
const errorState: {} = {
[keys[0]]: {
errorMessage: 'error',
resourceUUIDs: uuids
}
};
deepFreeze(errorState);
it('should return the current state when no valid actions have been made', () => {
const action = new NullAction();
const newState = responseCacheReducer(testState, action);
expect(newState).toEqual(testState);
});
it('should start with an empty cache', () => {
const action = new NullAction();
const initialState = responseCacheReducer(undefined, action);
expect(initialState).toEqual(Object.create(null));
});
describe('ADD', () => {
const addTimeAdded = new Date().getTime();
const addMsToLive = 5;
const addResponse = new RestResponse(true, '200');
const action = new ResponseCacheAddAction(keys[0], addResponse, addTimeAdded, addMsToLive);
it('should perform the action without affecting the previous state', () => {
// testState has already been frozen above
responseCacheReducer(testState, action);
});
it('should add the response to the cached request', () => {
const newState = responseCacheReducer(testState, action);
expect(newState[keys[0]].timeAdded).toBe(addTimeAdded);
expect(newState[keys[0]].msToLive).toBe(addMsToLive);
expect(newState[keys[0]].response).toBe(addResponse);
});
});
describe('REMOVE', () => {
it('should perform the action without affecting the previous state', () => {
const action = new ResponseCacheRemoveAction(keys[0]);
// testState has already been frozen above
responseCacheReducer(testState, action);
});
it('should remove the specified request from the cache', () => {
const action = new ResponseCacheRemoveAction(keys[0]);
const newState = responseCacheReducer(testState, action);
expect(testState[keys[0]]).not.toBeUndefined();
expect(newState[keys[0]]).toBeUndefined();
});
it('shouldn\'t do anything when the specified key isn\'t cached', () => {
const wrongKey = 'this isn\'t cached';
const action = new ResponseCacheRemoveAction(wrongKey);
const newState = responseCacheReducer(testState, action);
expect(testState[wrongKey]).toBeUndefined();
expect(newState).toEqual(testState);
});
});
describe('RESET_TIMESTAMPS', () => {
const newTimeStamp = new Date().getTime();
const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
it('should perform the action without affecting the previous state', () => {
// testState has already been frozen above
responseCacheReducer(testState, action);
});
it('should set the timestamp of all requests in the cache', () => {
const newState = responseCacheReducer(testState, action);
Object.keys(newState).forEach((key) => {
expect(newState[key].timeAdded).toEqual(newTimeStamp);
});
});
});
});

View File

@@ -1,111 +0,0 @@
import {
ResponseCacheAction, ResponseCacheActionTypes,
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
ResponseCacheAddAction
} from './response-cache.actions';
import { CacheEntry } from './cache-entry';
import { hasValue } from '../../shared/empty.util';
import { RestResponse } from './response-cache.models';
/**
* An entry in the ResponseCache
*/
export class ResponseCacheEntry implements CacheEntry {
key: string;
response: RestResponse;
timeAdded: number;
msToLive: number;
}
/**
* The ResponseCache State
*/
export interface ResponseCacheState {
[key: string]: ResponseCacheEntry
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
/**
* The ResponseCache Reducer
*
* @param state
* the current state
* @param action
* the action to perform on the state
* @return ResponseCacheState
* the new state
*/
export function responseCacheReducer(state = initialState, action: ResponseCacheAction): ResponseCacheState {
switch (action.type) {
case ResponseCacheActionTypes.ADD: {
return addToCache(state, action as ResponseCacheAddAction);
}
case ResponseCacheActionTypes.REMOVE: {
return removeFromCache(state, action as ResponseCacheRemoveAction);
}
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
return resetResponseCacheTimestamps(state, action as ResetResponseCacheTimestampsAction)
}
default: {
return state;
}
}
}
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
return Object.assign({}, state, {
[action.payload.key]: {
key: action.payload.key,
response: action.payload.response,
timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive
}
});
}
/**
* Remove a request from the cache
*
* @param state
* the current state
* @param action
* an ResponseCacheRemoveAction
* @return ResponseCacheState
* the new state, with the request removed if it existed.
*/
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
if (hasValue(state[action.payload])) {
const newCache = Object.assign({}, state);
delete newCache[action.payload];
return newCache;
} else {
return state;
}
}
/**
* Set the timeAdded timestamp of every cached request to the specified value
*
* @param state
* the current state
* @param action
* a ResetResponseCacheTimestampsAction
* @return ResponseCacheState
* the new state, with all timeAdded timestamps set to the specified value
*/
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
const newState = Object.create(null);
Object.keys(state).forEach((key) => {
newState[key] = Object.assign({}, state[key], {
timeAdded: action.payload
});
});
return newState;
}

View File

@@ -1,100 +0,0 @@
import { Store } from '@ngrx/store';
import { ResponseCacheService } from './response-cache.service';
import { of as observableOf } from 'rxjs';
import { CoreState } from '../core.reducers';
import { RestResponse } from './response-cache.models';
import { ResponseCacheEntry } from './response-cache.reducer';
import { first } from 'rxjs/operators';
import * as ngrx from '@ngrx/store'
import { cold } from 'jasmine-marbles';
describe('ResponseCacheService', () => {
let service: ResponseCacheService;
let store: Store<CoreState>;
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
const timestamp = new Date().getTime();
const validCacheEntry = (key) => {
return {
key: key,
response: new RestResponse(true, '200'),
timeAdded: timestamp,
msToLive: 24 * 60 * 60 * 1000 // a day
}
};
const invalidCacheEntry = (key) => {
return {
key: key,
response: new RestResponse(true, '200'),
timeAdded: 0,
msToLive: 0
}
};
beforeEach(() => {
store = new Store<CoreState>(undefined, undefined, undefined);
spyOn(store, 'dispatch');
service = new ResponseCacheService(store);
spyOn(Date.prototype, 'getTime').and.callFake(() => {
return timestamp;
});
});
describe('get', () => {
it('should return an observable of the cached request with the specified key', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(validCacheEntry(keys[1]));
};
});
let testObj: ResponseCacheEntry;
service.get(keys[1]).pipe(first()).subscribe((entry) => {
testObj = entry;
});
expect(testObj.key).toEqual(keys[1]);
});
it('should not return a cached request that has exceeded its time to live', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(invalidCacheEntry(keys[1]));
};
});
let getObsHasFired = false;
const subscription = service.get(keys[1]).subscribe((entry) => getObsHasFired = true);
expect(getObsHasFired).toBe(false);
subscription.unsubscribe();
});
});
describe('has', () => {
it('should return true if the request with the supplied key is cached and still valid', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(validCacheEntry(keys[1]));
};
});
expect(service.has(keys[1])).toBe(true);
});
it('should return false if the request with the supplied key isn\'t cached', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(undefined);
};
});
expect(service.has(keys[1])).toBe(false);
});
it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => {
spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(invalidCacheEntry(keys[1]));
};
});
expect(service.has(keys[1])).toBe(false);
});
});
});

View File

@@ -1,100 +0,0 @@
import { filter, take, distinctUntilChanged, first } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { ResponseCacheEntry } from './response-cache.reducer';
import { hasNoValue } from '../../shared/empty.util';
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
import { RestResponse } from './response-cache.models';
import { coreSelector, CoreState } from '../core.reducers';
import { pathSelector } from '../shared/selectors';
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'cache/response', key);
}
/**
* A service to interact with the response cache
*/
@Injectable()
export class ResponseCacheService {
constructor(
private store: Store<CoreState>
) {
}
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
if (!this.has(key)) {
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
}
return this.get(key);
}
/**
* Get an observable of the response with the specified key
*
* @param key
* the key of the response to get
* @return Observable<ResponseCacheEntry>
* an observable of the ResponseCacheEntry with the specified key
*/
get(key: string): Observable<ResponseCacheEntry> {
return this.store.pipe(
select(entryFromKeySelector(key)),
filter((entry: ResponseCacheEntry) => this.isValid(entry)),
distinctUntilChanged()
)
}
/**
* Check whether the response with the specified key is cached
*
* @param key
* the key of the response to check
* @return boolean
* true if the response with the specified key is cached,
* false otherwise
*/
has(key: string): boolean {
let result: boolean;
this.store.pipe(select(entryFromKeySelector(key)),
first()
).subscribe((entry: ResponseCacheEntry) => {
result = this.isValid(entry);
});
return result;
}
remove(key: string): void {
if (this.has(key)) {
this.store.dispatch(new ResponseCacheRemoveAction(key));
}
}
/**
* Check whether a ResponseCacheEntry should still be cached
*
* @param entry
* the entry to check
* @return boolean
* false if the entry is null, undefined, or its time to
* live has been exceeded, true otherwise
*/
private isValid(entry: ResponseCacheEntry): boolean {
if (hasNoValue(entry)) {
return false;
} else {
const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) {
this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
}
return !isOutDated;
}
}
}

View File

@@ -13,7 +13,7 @@ import { AuthStatus } from '../auth/models/auth-status.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class RestResponse { export class RestResponse {
public toCache = true; public timeAdded: number;
constructor( constructor(
public isSuccessful: boolean, public isSuccessful: boolean,

View File

@@ -1,7 +1,6 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ConfigRequest, FindAllOptions } from '../data/request.models'; import { ConfigRequest, FindAllOptions } from '../data/request.models';
@@ -16,7 +15,6 @@ class TestService extends ConfigService {
protected browseEndpoint = BROWSE; protected browseEndpoint = BROWSE;
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();
@@ -26,7 +24,6 @@ class TestService extends ConfigService {
describe('ConfigService', () => { describe('ConfigService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: TestService; let service: TestService;
let responseCache: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let halService: any; let halService: any;
@@ -39,17 +36,9 @@ describe('ConfigService', () => {
const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const scopedEndpoint = `${serviceEndpoint}/${scopeName}`;
const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`;
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', {
get: cold('c-', {
c: { response: { isSuccessful } }
})
});
}
function initTestService(): TestService { function initTestService(): TestService {
return new TestService( return new TestService(
responseCache,
requestService, requestService,
halService halService
); );
@@ -57,7 +46,6 @@ describe('ConfigService', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
halService = new HALEndpointServiceStub(configEndpoint); halService = new HALEndpointServiceStub(configEndpoint);
service = initTestService(); service = initTestService();

View File

@@ -1,24 +1,25 @@
import { Observable, of as observableOf, throwError as observableThrowError, merge as observableMerge } from 'rxjs'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ConfigSuccessResponse } from '../cache/response.models';
import { ConfigSuccessResponse } from '../cache/response-cache.models';
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ConfigData } from './config-data'; import { ConfigData } from './config-data';
import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
export abstract class ConfigService { export abstract class ConfigService {
protected request: ConfigRequest; protected request: ConfigRequest;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract linkPath: string; protected abstract linkPath: string;
protected abstract browseEndpoint: string; protected abstract browseEndpoint: string;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
protected getConfig(request: RestRequest): Observable<ConfigData> { protected getConfig(request: RestRequest): Observable<ConfigData> {
const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response)); const responses = this.requestService.getByHref(request.href).pipe(
getResponseFromEntry()
);
const errorResponses = responses.pipe( const errorResponses = responses.pipe(
filter((response) => !response.isSuccessful), filter((response) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`))) mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`)))
@@ -94,7 +95,6 @@ export abstract class ConfigService {
} }
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> { public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
console.log(this.halService.getEndpoint(this.linkPath));
return this.halService.getEndpoint(this.linkPath).pipe( return this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getConfigSearchHref(endpoint, options)), map((endpoint: string) => this.getConfigSearchHref(endpoint, options)),
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -11,7 +10,6 @@ export class SubmissionDefinitionsConfigService extends ConfigService {
protected browseEndpoint = 'search/findByCollection'; protected browseEndpoint = 'search/findByCollection';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -11,7 +10,6 @@ export class SubmissionFormsConfigService extends ConfigService {
protected browseEndpoint = ''; protected browseEndpoint = '';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -11,7 +10,6 @@ export class SubmissionSectionsConfigService extends ConfigService {
protected browseEndpoint = ''; protected browseEndpoint = '';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();

View File

@@ -1,13 +1,11 @@
import { ObjectCacheEffects } from './cache/object-cache.effects'; import { ObjectCacheEffects } from './cache/object-cache.effects';
import { ResponseCacheEffects } from './cache/response-cache.effects';
import { UUIDIndexEffects } from './index/index.effects'; import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects'; import { RequestEffects } from './data/request.effects';
import { AuthEffects } from './auth/auth.effects'; import { AuthEffects } from './auth/auth.effects';
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
export const coreEffects = [ export const coreEffects = [
ResponseCacheEffects,
RequestEffects, RequestEffects,
ObjectCacheEffects, ObjectCacheEffects,
UUIDIndexEffects, UUIDIndexEffects,

View File

@@ -32,7 +32,6 @@ import { ObjectCacheService } from './cache/object-cache.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
import { RequestService } from './data/request.service'; import { RequestService } from './data/request.service';
import { ResponseCacheService } from './cache/response-cache.service';
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
import { ServerResponseService } from '../shared/services/server-response.service'; import { ServerResponseService } from '../shared/services/server-response.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service';
@@ -102,7 +101,6 @@ const PROVIDERS = [
RegistryService, RegistryService,
RemoteDataBuildService, RemoteDataBuildService,
RequestService, RequestService,
ResponseCacheService,
EndpointMapResponseParsingService, EndpointMapResponseParsingService,
FacetValueResponseParsingService, FacetValueResponseParsingService,
FacetValueMapResponseParsingService, FacetValueMapResponseParsingService,

View File

@@ -1,6 +1,5 @@
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { indexReducer, IndexState } from './index/index.reducer'; import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer'; import { requestReducer, RequestState } from './data/request.reducer';
@@ -9,7 +8,6 @@ import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-s
export interface CoreState { export interface CoreState {
'cache/object': ObjectCacheState, 'cache/object': ObjectCacheState,
'cache/response': ResponseCacheState,
'cache/syncbuffer': ServerSyncBufferState, 'cache/syncbuffer': ServerSyncBufferState,
'data/request': RequestState, 'data/request': RequestState,
'index': IndexState, 'index': IndexState,
@@ -18,7 +16,6 @@ export interface CoreState {
export const coreReducers: ActionReducerMap<CoreState> = { export const coreReducers: ActionReducerMap<CoreState> = {
'cache/object': objectCacheReducer, 'cache/object': objectCacheReducer,
'cache/response': responseCacheReducer,
'cache/syncbuffer': serverSyncBufferReducer, 'cache/syncbuffer': serverSyncBufferReducer,
'data/request': requestReducer, 'data/request': requestReducer,
'index': indexReducer, 'index': indexReducer,

View File

@@ -1,5 +1,5 @@
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { BrowseEntriesRequest } from './request.models'; import { BrowseEntriesRequest } from './request.models';

View File

@@ -7,7 +7,7 @@ import {
ErrorResponse, ErrorResponse,
GenericSuccessResponse, GenericSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BrowseEntry } from '../shared/browse-entry.model'; import { BrowseEntry } from '../shared/browse-entry.model';

View File

@@ -1,5 +1,5 @@
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models'; import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models';

View File

@@ -7,7 +7,7 @@ import {
ErrorResponse, ErrorResponse,
GenericSuccessResponse, GenericSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BaseResponseParsingService } from './base-response-parsing.service'; import { BaseResponseParsingService } from './base-response-parsing.service';

View File

@@ -1,6 +1,6 @@
import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { BrowseEndpointRequest } from './request.models'; import { BrowseEndpointRequest } from './request.models';
import { GenericSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; import { GenericSuccessResponse, ErrorResponse } from '../cache/response.models';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';

View File

@@ -1,10 +1,8 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { NormalizedCollection } from '../cache/models/normalized-collection.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
@@ -17,7 +15,6 @@ export class CollectionDataService extends ComColDataService<NormalizedCollectio
protected linkPath = 'collections'; protected linkPath = 'collections';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,

View File

@@ -5,7 +5,6 @@ import { GlobalConfig } from '../../../config';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
@@ -23,7 +22,6 @@ class NormalizedTestObject extends NormalizedObject {
class TestService extends ComColDataService<NormalizedTestObject, any> { class TestService extends ComColDataService<NormalizedTestObject, any> {
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
@@ -41,7 +39,6 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
describe('ComColDataService', () => { describe('ComColDataService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: TestService; let service: TestService;
let responseCache: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let cds: CommunityDataService; let cds: CommunityDataService;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
@@ -68,14 +65,6 @@ describe('ComColDataService', () => {
}); });
} }
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', {
get: cold('c-', {
c: { response: { isSuccessful } }
})
});
}
function initMockObjectCacheService(): ObjectCacheService { function initMockObjectCacheService(): ObjectCacheService {
return jasmine.createSpyObj('objectCache', { return jasmine.createSpyObj('objectCache', {
getByUUID: cold('d-', { getByUUID: cold('d-', {
@@ -90,7 +79,6 @@ describe('ComColDataService', () => {
function initTestService(): TestService { function initTestService(): TestService {
return new TestService( return new TestService(
responseCache,
requestService, requestService,
rdbService, rdbService,
store, store,
@@ -111,7 +99,6 @@ describe('ComColDataService', () => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = getMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(true);
service = initTestService(); service = initTestService();
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
@@ -127,7 +114,6 @@ describe('ComColDataService', () => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = getMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(true);
service = initTestService(); service = initTestService();
}); });
@@ -150,7 +136,6 @@ describe('ComColDataService', () => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = getMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(false);
service = initTestService(); service = initTestService();
}); });

View File

@@ -1,15 +1,16 @@
import { distinctUntilChanged, filter, map, mergeMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, share, take, tap } from 'rxjs/operators';
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { FindAllOptions, FindByIDRequest } from './request.models'; import { FindAllOptions, FindByIDRequest } from './request.models';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestEntry } from './request.reducer';
import { getResponseFromEntry } from '../shared/operators';
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> { export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
protected abstract cds: CommunityDataService; protected abstract cds: CommunityDataService;
@@ -26,9 +27,9 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
* @return { Observable<string> } * @return { Observable<string> }
* an Observable<string> containing the scoped URL * an Observable<string> containing the scoped URL
*/ */
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> { public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
if (isEmpty(options.scopeID)) { if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(this.linkPath); return this.halService.getEndpoint(linkPath);
} else { } else {
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)), mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)),
@@ -37,7 +38,7 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
tap((href: string) => { tap((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request); this.requestService.configure(request);
}),); }));
// return scopeCommunityHrefObs.pipe( // return scopeCommunityHrefObs.pipe(
// mergeMap((href: string) => this.responseCache.get(href)), // mergeMap((href: string) => this.responseCache.get(href)),
@@ -46,7 +47,7 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
// if (response.isSuccessful) { // if (response.isSuccessful) {
// const community$: Observable<NormalizedCommunity> = this.objectCache.getByUUID(scopeID); // const community$: Observable<NormalizedCommunity> = this.objectCache.getByUUID(scopeID);
// return community$.pipe( // return community$.pipe(
// map((community) => community._links[this.linkPath]), // map((community) => community._links[linkPath]),
// filter((href) => isNotEmpty(href)), // filter((href) => isNotEmpty(href)),
// distinctUntilChanged() // distinctUntilChanged()
// ); // );
@@ -57,8 +58,8 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
// distinctUntilChanged() // distinctUntilChanged()
// ); // );
const responses = scopeCommunityHrefObs.pipe( const responses = scopeCommunityHrefObs.pipe(
mergeMap((href: string) => this.responseCache.get(href)), mergeMap((href: string) => this.requestService.getByHref(href)),
map((entry: ResponseCacheEntry) => entry.response)); getResponseFromEntry());
const errorResponses = responses.pipe( const errorResponses = responses.pipe(
filter((response) => !response.isSuccessful), filter((response) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))) mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
@@ -66,11 +67,11 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
const successResponses = responses.pipe( const successResponses = responses.pipe(
filter((response) => response.isSuccessful), filter((response) => response.isSuccessful),
mergeMap(() => this.objectCache.getByUUID(options.scopeID)), mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
map((nc: NormalizedCommunity) => nc._links[this.linkPath]), map((nc: NormalizedCommunity) => nc._links[linkPath]),
filter((href) => isNotEmpty(href)) filter((href) => isNotEmpty(href))
); );
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged()); return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share());
} }
} }
} }

View File

@@ -1,12 +1,10 @@
import { filter, mergeMap, take } from 'rxjs/operators';
import {mergeMap, filter, take} from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { Community } from '../shared/community.model'; import { Community } from '../shared/community.model';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
@@ -25,7 +23,6 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
protected cds = this; protected cds = this;
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
@@ -40,12 +37,10 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
} }
findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> { findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
const hrefObs = this.halService.getEndpoint(this.topLinkPath).pipe(filter((href: string) => isNotEmpty(href)), const hrefObs = this.getFindAllHref(options, this.topLinkPath);
mergeMap((endpoint: string) => this.getFindAllHref(options)),);
hrefObs.pipe( hrefObs.pipe(
filter((href: string) => hasValue(href)), filter((href: string) => hasValue(href)),
take(1),) take(1))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request); this.requestService.configure(request);

View File

@@ -1,4 +1,4 @@
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models';
import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { ConfigObjectFactory } from '../shared/config/config-object-factory'; import { ConfigObjectFactory } from '../shared/config/config-object-factory';

View File

@@ -1,6 +1,5 @@
import { DataService } from './data.service'; import { DataService } from './data.service';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -22,7 +21,6 @@ class NormalizedTestObject extends NormalizedObject {
class TestService extends DataService<NormalizedTestObject, any> { class TestService extends DataService<NormalizedTestObject, any> {
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
@@ -33,7 +31,7 @@ class TestService extends DataService<NormalizedTestObject, any> {
super(); super();
} }
public getBrowseEndpoint(options: FindAllOptions): Observable<string> { public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return observableOf(endpoint); return observableOf(endpoint);
} }
} }
@@ -41,7 +39,6 @@ class TestService extends DataService<NormalizedTestObject, any> {
describe('DataService', () => { describe('DataService', () => {
let service: TestService; let service: TestService;
let options: FindAllOptions; let options: FindAllOptions;
const responseCache = {} as ResponseCacheService;
const requestService = {} as RequestService; const requestService = {} as RequestService;
const halService = {} as HALEndpointService; const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService; const rdbService = {} as RemoteDataBuildService;
@@ -57,7 +54,6 @@ describe('DataService', () => {
function initTestService(): TestService { function initTestService(): TestService {
return new TestService( return new TestService(
responseCache,
requestService, requestService,
rdbService, rdbService,
store, store,

View File

@@ -1,9 +1,8 @@
import { distinctUntilChanged, filter, first, map, take } from 'rxjs/operators'; import { delay, distinctUntilChanged, filter, first, map, take, tap } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
@@ -15,9 +14,9 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
import { compare, Operation } from 'fast-json-patch'; import { compare, Operation } from 'fast-json-patch';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { of } from 'rxjs/internal/observable/of';
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> { export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract rdbService: RemoteDataBuildService; protected abstract rdbService: RemoteDataBuildService;
protected abstract store: Store<CoreState>; protected abstract store: Store<CoreState>;
@@ -25,34 +24,31 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
protected abstract objectCache: ObjectCacheService; protected abstract objectCache: ObjectCacheService;
public abstract getBrowseEndpoint(options: FindAllOptions): Observable<string> public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable<string>
protected getFindAllHref(options: FindAllOptions = {}): Observable<string> { protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
let result: Observable<string>; let result: Observable<string>;
const args = []; const args = [];
result = this.getBrowseEndpoint(options).pipe(distinctUntilChanged()); result = this.getBrowseEndpoint(options, linkPath);
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args.push(`page=${options.currentPage - 1}`); args.push(`page=${options.currentPage - 1}`);
} }
if (hasValue(options.elementsPerPage)) { if (hasValue(options.elementsPerPage)) {
args.push(`size=${options.elementsPerPage}`); args.push(`size=${options.elementsPerPage}`);
} }
if (hasValue(options.sort)) { if (hasValue(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`); args.push(`sort=${options.sort.field},${options.sort.direction}`);
} }
if (hasValue(options.startsWith)) { if (hasValue(options.startsWith)) {
args.push(`startsWith=${options.startsWith}`); args.push(`startsWith=${options.startsWith}`);
} }
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()));
} else { } else {
result.subscribe((t) => console.log(t));
return result; return result;
} }
} }
@@ -115,6 +111,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
this.objectCache.addPatch(object.self, operations); this.objectCache.addPatch(object.self, operations);
} }
} }
// TODO implement, after the structure of the REST server's POST response is finalized // TODO implement, after the structure of the REST server's POST response is finalized
// create(dso: DSpaceObject): Observable<RemoteData<TDomain>> { // create(dso: DSpaceObject): Observable<RemoteData<TDomain>> {
// const postHrefObs = this.getEndpoint(); // const postHrefObs = this.getEndpoint();

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RestResponse } from '../cache/response-cache.models'; import { RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';

View File

@@ -7,7 +7,7 @@ import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models'; import { RestResponse, DSOSuccessResponse } from '../cache/response.models';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
@@ -23,12 +23,14 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem
constructor( constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
) { super(); ) {
super();
} }
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href); const processRequestDTO = this.process<NormalizedObject, ResourceType>(data.payload, request.href);
let objectList = processRequestDTO; let objectList = processRequestDTO;
if (hasNoValue(processRequestDTO)) { if (hasNoValue(processRequestDTO)) {
return new DSOSuccessResponse([], data.statusCode, undefined) return new DSOSuccessResponse([], data.statusCode, undefined)
} }

View File

@@ -3,7 +3,6 @@ import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -18,7 +17,6 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
protected linkPath = 'dso'; protected linkPath = 'dso';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
@@ -27,8 +25,8 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
super(); super();
} }
getBrowseEndpoint(options: FindAllOptions): Observable<string> { getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
return this.halService.getEndpoint(this.linkPath); return this.halService.getEndpoint(linkPath);
} }
getFindByIDHref(endpoint, resourceID): string { getFindByIDHref(endpoint, resourceID): string {
@@ -46,7 +44,7 @@ export class DSpaceObjectDataService {
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
protected objectCache: ObjectCacheService) { protected objectCache: ObjectCacheService) {
this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, objectCache); this.dataService = new DataServiceImpl(requestService, rdbService, null, halService, objectCache);
} }
findById(uuid: string): Observable<RemoteData<DSpaceObject>> { findById(uuid: string): Observable<RemoteData<DSpaceObject>> {

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
import { import {
FacetConfigSuccessResponse, FacetConfigSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';

View File

@@ -4,7 +4,7 @@ import {
FacetValueMapSuccessResponse, FacetValueMapSuccessResponse,
FacetValueSuccessResponse, FacetValueSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';

View File

@@ -4,7 +4,7 @@ import {
FacetValueMapSuccessResponse, FacetValueMapSuccessResponse,
FacetValueSuccessResponse, FacetValueSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';

View File

@@ -3,7 +3,6 @@ import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { BrowseService } from '../browse/browse.service'; import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
@@ -16,7 +15,6 @@ describe('ItemDataService', () => {
let service: ItemDataService; let service: ItemDataService;
let bs: BrowseService; let bs: BrowseService;
const requestService = {} as RequestService; const requestService = {} as RequestService;
const responseCache = {} as ResponseCacheService;
const rdbService = {} as RemoteDataBuildService; const rdbService = {} as RemoteDataBuildService;
const objectCache = {} as ObjectCacheService; const objectCache = {} as ObjectCacheService;
const store = {} as Store<CoreState>; const store = {} as Store<CoreState>;
@@ -48,7 +46,6 @@ describe('ItemDataService', () => {
function initTestService() { function initTestService() {
return new ItemDataService( return new ItemDataService(
responseCache,
requestService, requestService,
rdbService, rdbService,
store, store,

View File

@@ -7,7 +7,6 @@ import { isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../browse/browse.service'; import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedItem } from '../cache/models/normalized-item.model'; import { NormalizedItem } from '../cache/models/normalized-item.model';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
@@ -23,7 +22,6 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
protected linkPath = 'items'; protected linkPath = 'items';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
@@ -39,12 +37,12 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
* @param {FindAllOptions} options * @param {FindAllOptions} options
* @returns {Observable<string>} * @returns {Observable<string>}
*/ */
public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> { public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> {
let field = 'dc.date.issued'; let field = 'dc.date.issued';
if (options.sort && options.sort.field) { if (options.sort && options.sort.field) {
field = options.sort.field; field = options.sort.field;
} }
return this.bs.getBrowseURLFor(field, this.linkPath).pipe( return this.bs.getBrowseURLFor(field, linkPath).pipe(
filter((href: string) => isNotEmpty(href)), filter((href: string) => isNotEmpty(href)),
map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()), map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()),
distinctUntilChanged(),); distinctUntilChanged(),);

View File

@@ -4,7 +4,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
@Injectable() @Injectable()
export class MetadataschemaParsingService implements ResponseParsingService { export class MetadataschemaParsingService implements ResponseParsingService {

View File

@@ -1,6 +1,6 @@
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { RestResponse } from '../cache/response-cache.models'; import { RestResponse } from '../cache/response.models';
export interface ResponseParsingService { export interface ResponseParsingService {
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse; parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse;

View File

@@ -1,4 +1,4 @@
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';

View File

@@ -1,7 +1,7 @@
import { import {
RegistryMetadatafieldsSuccessResponse, RegistryMetadatafieldsSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';

View File

@@ -1,4 +1,4 @@
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';

View File

@@ -1,6 +1,7 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { RestResponse } from '../cache/response.models';
/** /**
* The list of RequestAction type definitions * The list of RequestAction type definitions
@@ -8,7 +9,8 @@ import { RestRequest } from './request.models';
export const RequestActionTypes = { export const RequestActionTypes = {
CONFIGURE: type('dspace/core/data/request/CONFIGURE'), CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
EXECUTE: type('dspace/core/data/request/EXECUTE'), EXECUTE: type('dspace/core/data/request/EXECUTE'),
COMPLETE: type('dspace/core/data/request/COMPLETE') COMPLETE: type('dspace/core/data/request/COMPLETE'),
RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS')
}; };
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -43,7 +45,10 @@ export class RequestExecuteAction implements Action {
*/ */
export class RequestCompleteAction implements Action { export class RequestCompleteAction implements Action {
type = RequestActionTypes.COMPLETE; type = RequestActionTypes.COMPLETE;
payload: string; payload: {
uuid: string,
response: RestResponse
};
/** /**
* Create a new RequestCompleteAction * Create a new RequestCompleteAction
@@ -51,10 +56,32 @@ export class RequestCompleteAction implements Action {
* @param uuid * @param uuid
* the request's uuid * the request's uuid
*/ */
constructor(uuid: string) { constructor(uuid: string, response: RestResponse) {
this.payload = uuid; this.payload = {
uuid,
response
};
} }
} }
/**
* An ngrx action to reset the timeAdded property of all responses in the cached objects
*/
export class ResetResponseTimestampsAction implements Action {
type = RequestActionTypes.RESET_TIMESTAMPS;
payload: number;
/**
* Create a new ResetResponseTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) {
this.payload = newTimestamp;
}
}
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */
/** /**
@@ -63,4 +90,5 @@ export class RequestCompleteAction implements Action {
export type RequestAction export type RequestAction
= RequestConfigureAction = RequestConfigureAction
| RequestExecuteAction | RequestExecuteAction
| RequestCompleteAction; | RequestCompleteAction
| ResetResponseTimestampsAction;

View File

@@ -1,30 +1,33 @@
import { Observable, of as observableOf } from 'rxjs';
import {of as observableOf, Observable } from 'rxjs';
import { Inject, Injectable, Injector } from '@angular/core'; import { Inject, Injectable, Injector } from '@angular/core';
import { Request } from '@angular/http';
import { RequestArgs } from '@angular/http/src/interfaces';
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { ResponseCacheService } from '../cache/response-cache.service';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions'; import {
RequestActionTypes,
RequestCompleteAction,
RequestExecuteAction,
ResetResponseTimestampsAction
} from './request.actions';
import { RequestError, RestRequest } from './request.models'; import { RequestError, RestRequest } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { catchError, flatMap, map, take, tap } from 'rxjs/operators'; import { catchError, flatMap, map, take, tap } from 'rxjs/operators';
import { ErrorResponse, RestResponse } from '../cache/response.models';
import { StoreActionTypes } from '../../store.actions';
export const addToResponseCacheAndCompleteAction = (request: RestRequest, responseCache: ResponseCacheService, envConfig: GlobalConfig) => export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) =>
(source: Observable<ErrorResponse>): Observable<RequestCompleteAction> => (source: Observable<RestResponse>): Observable<RequestCompleteAction> =>
source.pipe( source.pipe(
tap((response: RestResponse) => responseCache.add(request.href, response, request.responseMsToLive ? request.responseMsToLive : envConfig.cache.msToLive.default)), map((response: RestResponse) => {
map((response: RestResponse) => new RequestCompleteAction(request.uuid)) return new RequestCompleteAction(request.uuid, response)
})
); );
@Injectable() @Injectable()
@@ -46,20 +49,32 @@ export class RequestEffects {
} }
return this.restApi.request(request.method, request.href, body, request.options).pipe( return this.restApi.request(request.method, request.href, body, request.options).pipe(
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)), map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig), addToResponseCacheAndCompleteAction(request, this.EnvConfig),
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe( catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig) addToResponseCacheAndCompleteAction(request, this.EnvConfig)
)) ))
); );
}) })
); );
/**
* When the store is rehydrated in the browser, set all cache
* timestamps to 'now', because the time zone of the server can
* differ from the client.
*
* This assumes that the server cached everything a negligible
* time ago, and will likely need to be revisited later
*/
@Effect() fixTimestampsOnRehydrate = this.actions$
.pipe(ofType(StoreActionTypes.REHYDRATE),
map(() => new ResetResponseTimestampsAction(new Date().getTime()))
);
constructor( constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions, private actions$: Actions,
private restApi: DSpaceRESTv2Service, private restApi: DSpaceRESTv2Service,
private injector: Injector, private injector: Injector,
private responseCache: ResponseCacheService,
protected requestService: RequestService protected requestService: RequestService
) { } ) { }

View File

@@ -14,32 +14,36 @@ import { BrowseItemsResponseParsingService } from './browse-items-response-parsi
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export abstract class RestRequest { export abstract class RestRequest {
public responseMsToLive = 0;
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public method: RestRequestMethod = RestRequestMethod.GET, public method: RestRequestMethod = RestRequestMethod.GET,
public body?: any, public body?: any,
public options?: HttpOptions, public options?: HttpOptions,
public responseMsToLive?: number
) { ) {
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
return DSOResponseParsingService; return DSOResponseParsingService;
} }
get toCache(): boolean {
return this.responseMsToLive > 0;
}
} }
export class GetRequest extends RestRequest { export class GetRequest extends RestRequest {
public responseMsToLive = 60 * 15 * 1000;
constructor( constructor(
public uuid: string, public uuid: string,
public href: string, public href: string,
public body?: any, public body?: any,
public options?: HttpOptions, public options?: HttpOptions,
public responseMsToLive?: number
) { ) {
super(uuid, href, RestRequestMethod.GET, body, options, responseMsToLive) super(uuid, href, RestRequestMethod.GET, body, options)
} }
} }
@@ -212,6 +216,7 @@ export class IntegrationRequest extends GetRequest {
return IntegrationResponseParsingService; return IntegrationResponseParsingService;
} }
} }
export class RequestError extends Error { export class RequestError extends Error {
statusText: string; statusText: string;
} }

View File

@@ -1,14 +1,16 @@
import { import {
RequestActionTypes, RequestAction, RequestConfigureAction, RequestActionTypes, RequestAction, RequestConfigureAction,
RequestExecuteAction, RequestCompleteAction RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction
} from './request.actions'; } from './request.actions';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { RestResponse } from '../cache/response.models';
export class RequestEntry { export class RequestEntry {
request: RestRequest; request: RestRequest;
requestPending: boolean; requestPending: boolean;
responsePending: boolean; responsePending: boolean;
completed: boolean; completed: boolean;
response: RestResponse
} }
export interface RequestState { export interface RequestState {
@@ -32,6 +34,9 @@ export function requestReducer(state = initialState, action: RequestAction): Req
case RequestActionTypes.COMPLETE: { case RequestActionTypes.COMPLETE: {
return completeRequest(state, action as RequestCompleteAction); return completeRequest(state, action as RequestCompleteAction);
} }
case RequestActionTypes.RESET_TIMESTAMPS: {
return resetResponseTimestamps(state, action as ResetResponseTimestampsAction);
}
default: { default: {
return state; return state;
@@ -45,7 +50,7 @@ function configureRequest(state: RequestState, action: RequestConfigureAction):
request: action.payload, request: action.payload,
requestPending: true, requestPending: true,
responsePending: false, responsePending: false,
completed: false completed: false,
} }
}); });
} }
@@ -70,10 +75,25 @@ function executeRequest(state: RequestState, action: RequestExecuteAction): Requ
* the new state, with the response added to the request * the new state, with the response added to the request
*/ */
function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState { function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
return Object.assign({}, state, { const time = new Date().getTime();
[action.payload]: Object.assign({}, state[action.payload], {
const ob = Object.assign({}, state, {
[action.payload.uuid]: Object.assign({}, state[action.payload.uuid], {
responsePending: false, responsePending: false,
completed: true completed: true,
response: Object.assign({}, action.payload.response, { timeAdded: time })
}) })
}); });
console.log(ob);
return ob;
}
function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) {
const newState = Object.create(null);
Object.keys(state).forEach((key) => {
newState[key] = Object.assign({}, state[key],
{ response: Object.assign({}, state[key].response, { timeAdded: action.payload }) }
);
});
return newState;
} }

View File

@@ -1,10 +1,8 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
@@ -29,7 +27,6 @@ describe('RequestService', () => {
let service: RequestService; let service: RequestService;
let serviceAsAny: any; let serviceAsAny: any;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
let responseCache: ResponseCacheService;
let uuidService: UUIDService; let uuidService: UUIDService;
let store: Store<CoreState>; let store: Store<CoreState>;
@@ -49,7 +46,6 @@ describe('RequestService', () => {
objectCache = getMockObjectCacheService(); objectCache = getMockObjectCacheService();
(objectCache.hasBySelfLink as any).and.returnValue(false); (objectCache.hasBySelfLink as any).and.returnValue(false);
responseCache = getMockResponseCacheService();
(responseCache.has as any).and.returnValue(false); (responseCache.has as any).and.returnValue(false);
(responseCache.get as any).and.returnValue(observableOf(undefined)); (responseCache.get as any).and.returnValue(observableOf(undefined));
@@ -65,7 +61,6 @@ describe('RequestService', () => {
service = new RequestService( service = new RequestService(
objectCache, objectCache,
responseCache,
uuidService, uuidService,
store store
); );

View File

@@ -1,14 +1,23 @@
import { Observable, merge as observableMerge } from 'rxjs'; import { merge as observableMerge, Observable, of as observableOf } from 'rxjs';
import { filter, first, map, mergeMap, partition, take } from 'rxjs/operators'; import {
filter,
find,
first,
map,
mergeMap,
reduce,
startWith,
switchMap,
take,
tap
} from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store'; import { MemoizedSelector, select, Store } from '@ngrx/store';
import { hasValue } from '../../shared/empty.util'; import { hasNoValue, hasValue } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { coreSelector, CoreState } from '../core.reducers'; import { coreSelector, CoreState } from '../core.reducers';
import { IndexName } from '../index/index.reducer'; import { IndexName } from '../index/index.reducer';
import { pathSelector } from '../shared/selectors'; import { pathSelector } from '../shared/selectors';
@@ -19,13 +28,13 @@ import { GetRequest, RestRequest } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { getResponseFromEntry } from '../shared/operators';
@Injectable() @Injectable()
export class RequestService { export class RequestService {
private requestsOnTheirWayToTheStore: string[] = []; private requestsOnTheirWayToTheStore: string[] = [];
constructor(private objectCache: ObjectCacheService, constructor(private objectCache: ObjectCacheService,
private responseCache: ResponseCacheService,
private uuidService: UUIDService, private uuidService: UUIDService,
private store: Store<CoreState>) { private store: Store<CoreState>) {
} }
@@ -83,23 +92,27 @@ export class RequestService {
private isCachedOrPending(request: GetRequest) { private isCachedOrPending(request: GetRequest) {
let isCached = this.objectCache.hasBySelfLink(request.href); let isCached = this.objectCache.hasBySelfLink(request.href);
if (!isCached && this.responseCache.has(request.href)) { const responses: Observable<RestResponse> = this.isReusable(request.uuid).pipe(
const responses = this.responseCache.get(request.href).pipe( filter((reusable: boolean) => !isCached && reusable),
take(1), switchMap(() => {
map((entry: ResponseCacheEntry) => entry.response) return this.getByHref(request.href).pipe(
); getResponseFromEntry(),
take(1)
);
}
));
const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error.
const dsoSuccessResponses = responses.pipe( const dsoSuccessResponses = responses.pipe(
filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)),
map((response: DSOSuccessResponse) => response.resourceSelfLinks), map((response: DSOSuccessResponse) => response.resourceSelfLinks),
map((resourceSelfLinks: string[]) => resourceSelfLinks map((resourceSelfLinks: string[]) => resourceSelfLinks
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) .every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
)); ));
const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true));
observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c);
observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c);
}
const isPending = this.isPending(request); const isPending = this.isPending(request);
return isCached || isPending; return isCached || isPending;
} }
@@ -129,4 +142,34 @@ export class RequestService {
commit(method?: RestRequestMethod) { commit(method?: RestRequestMethod) {
this.store.dispatch(new CommitSSBAction(method)) this.store.dispatch(new CommitSSBAction(method))
} }
/**
* Check whether a ResponseCacheEntry should still be cached
*
* @param entry
* the entry to check
* @return boolean
* false if the entry is null, undefined, or its time to
* live has been exceeded, true otherwise
*/
private isReusable(uuid: string): Observable<boolean> {
if (hasNoValue(uuid)) {
return observableOf(false);
} else {
const requestEntry$ = this.getByUUID(uuid);
return requestEntry$.pipe(
filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)),
map((entry: RequestEntry) => {
if (hasValue(entry) && entry.response.isSuccessful) {
const timeOutdated = entry.response.timeAdded + entry.request.responseMsToLive;
const isOutDated = new Date().getTime() > timeOutdated;
return !isOutDated;
} else {
return false;
}
})
);
return observableOf(false);
}
}
} }

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RestResponse, SearchSuccessResponse } from '../cache/response-cache.models'; import { RestResponse, SearchSuccessResponse } from '../cache/response.models';
import { DSOResponseParsingService } from './dso-response-parsing.service'; import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { IntegrationService } from './integration.service'; import { IntegrationService } from './integration.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -11,7 +10,6 @@ export class AuthorityService extends IntegrationService {
protected browseEndpoint = 'entries'; protected browseEndpoint = 'entries';
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();

View File

@@ -1,4 +1,4 @@
import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';

View File

@@ -6,7 +6,7 @@ import {
ErrorResponse, ErrorResponse,
IntegrationSuccessResponse, IntegrationSuccessResponse,
RestResponse RestResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { IntegrationObjectFactory } from './integration-object-factory'; import { IntegrationObjectFactory } from './integration-object-factory';

View File

@@ -1,7 +1,6 @@
import { cold, getTestScheduler } from 'jasmine-marbles'; import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { IntegrationRequest } from '../data/request.models'; import { IntegrationRequest } from '../data/request.models';
@@ -18,7 +17,6 @@ class TestService extends IntegrationService {
protected browseEndpoint = BROWSE; protected browseEndpoint = BROWSE;
constructor( constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected halService: HALEndpointService) { protected halService: HALEndpointService) {
super(); super();
@@ -28,7 +26,6 @@ class TestService extends IntegrationService {
describe('IntegrationService', () => { describe('IntegrationService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: TestService; let service: TestService;
let responseCache: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let halService: any; let halService: any;
let findOptions: IntegrationSearchOptions; let findOptions: IntegrationSearchOptions;
@@ -43,24 +40,14 @@ describe('IntegrationService', () => {
findOptions = new IntegrationSearchOptions(uuid, name, metadata); findOptions = new IntegrationSearchOptions(uuid, name, metadata);
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { function initTestService(): TestService {
return jasmine.createSpyObj('responseCache', {
get: cold('c-', {
c: {response: {isSuccessful}}
})
});
}
function initTestService(): TestService {
return new TestService( return new TestService(
responseCache,
requestService, requestService,
halService halService
); );
} }
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
scheduler = getTestScheduler(); scheduler = getTestScheduler();
halService = new HALEndpointServiceStub(integrationEndpoint); halService = new HALEndpointServiceStub(integrationEndpoint);

View File

@@ -1,27 +1,26 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { IntegrationSuccessResponse } from '../cache/response.models';
import { IntegrationSuccessResponse } from '../cache/response-cache.models';
import { GetRequest, IntegrationRequest } from '../data/request.models'; import { GetRequest, IntegrationRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { IntegrationData } from './integration-data'; import { IntegrationData } from './integration-data';
import { IntegrationSearchOptions } from './models/integration-options.model'; import { IntegrationSearchOptions } from './models/integration-options.model';
import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
export abstract class IntegrationService { export abstract class IntegrationService {
protected request: IntegrationRequest; protected request: IntegrationRequest;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract linkPath: string; protected abstract linkPath: string;
protected abstract browseEndpoint: string; protected abstract browseEndpoint: string;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
protected getData(request: GetRequest): Observable<IntegrationData> { protected getData(request: GetRequest): Observable<IntegrationData> {
return this.responseCache.get(request.href).pipe( return this.requestService.getByHref(request.href).pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
mergeMap((response) => { mergeMap((response) => {
if (response.isSuccessful && isNotEmpty(response)) { if (response.isSuccessful && isNotEmpty(response)) {
const dataResponse = response as IntegrationSuccessResponse; const dataResponse = response as IntegrationSuccessResponse;
return observableOf(new IntegrationData(dataResponse.pageInfo, dataResponse.dataDefinition)); return observableOf(new IntegrationData(dataResponse.pageInfo, dataResponse.dataDefinition));

View File

@@ -23,7 +23,6 @@ import { ItemDataService } from '../data/item-data.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
@@ -62,7 +61,6 @@ describe('MetadataService', () => {
let store: Store<CoreState>; let store: Store<CoreState>;
let objectCacheService: ObjectCacheService; let objectCacheService: ObjectCacheService;
let responseCacheService: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let uuidService: UUIDService; let uuidService: UUIDService;
let remoteDataBuildService: RemoteDataBuildService; let remoteDataBuildService: RemoteDataBuildService;
@@ -82,10 +80,9 @@ describe('MetadataService', () => {
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
objectCacheService = new ObjectCacheService(store); objectCacheService = new ObjectCacheService(store);
responseCacheService = new ResponseCacheService(store);
uuidService = new UUIDService(); uuidService = new UUIDService();
requestService = new RequestService(objectCacheService, responseCacheService, uuidService, store); requestService = new RequestService(objectCacheService, uuidService, store);
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, responseCacheService, requestService); remoteDataBuildService = new RemoteDataBuildService(objectCacheService, requestService);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -108,7 +105,6 @@ describe('MetadataService', () => {
], ],
providers: [ providers: [
{ provide: ObjectCacheService, useValue: objectCacheService }, { provide: ObjectCacheService, useValue: objectCacheService },
{ provide: ResponseCacheService, useValue: responseCacheService },
{ provide: RequestService, useValue: requestService }, { provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },

View File

@@ -1,24 +1,21 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RegistryService } from './registry.service'; import { RegistryService } from './registry.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { RequestEntry } from '../data/request.reducer'; import { RequestEntry } from '../data/request.reducer';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { import {
RegistryBitstreamformatsSuccessResponse, RegistryBitstreamformatsSuccessResponse,
RegistryMetadatafieldsSuccessResponse, RegistryMetadatafieldsSuccessResponse,
RegistryMetadataschemasSuccessResponse RegistryMetadataschemasSuccessResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
@@ -146,7 +143,6 @@ describe('RegistryService', () => {
DummyComponent DummyComponent
], ],
providers: [ providers: [
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: rdbStub }, { provide: RemoteDataBuildService, useValue: rdbStub },
{ provide: HALEndpointService, useValue: halServiceStub }, { provide: HALEndpointService, useValue: halServiceStub },

View File

@@ -6,29 +6,29 @@ import { PageInfo } from '../shared/page-info.model';
import { MetadataSchema } from '../metadata/metadataschema.model'; import { MetadataSchema } from '../metadata/metadataschema.model';
import { MetadataField } from '../metadata/metadatafield.model'; import { MetadataField } from '../metadata/metadatafield.model';
import { BitstreamFormat } from './mock-bitstream-format.model'; import { BitstreamFormat } from './mock-bitstream-format.model';
import { flatMap, map, tap } from 'rxjs/operators'; import { filter, flatMap, map, tap } from 'rxjs/operators';
import { GetRequest, RestRequest } from '../data/request.models'; import { GetRequest, RestRequest } from '../data/request.models';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { ResponseParsingService } from '../data/parsing.service'; import { ResponseParsingService } from '../data/parsing.service';
import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service'; import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { import {
RegistryBitstreamformatsSuccessResponse, RegistryBitstreamformatsSuccessResponse,
RegistryMetadatafieldsSuccessResponse, RegistryMetadatafieldsSuccessResponse,
RegistryMetadataschemasSuccessResponse RegistryMetadataschemasSuccessResponse
} from '../cache/response-cache.models'; } from '../cache/response.models';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
import { isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service'; import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model'; import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
import { RequestEntry } from '../data/request.reducer';
import { getResponseFromEntry } from '../shared/operators';
@Injectable() @Injectable()
export class RegistryService { export class RegistryService {
@@ -37,8 +37,7 @@ export class RegistryService {
private metadataFieldsPath = 'metadatafields'; private metadataFieldsPath = 'metadatafields';
private bitstreamFormatsPath = 'bitstreamformats'; private bitstreamFormatsPath = 'bitstreamformats';
constructor(protected responseCache: ResponseCacheService, constructor(protected requestService: RequestService,
protected requestService: RequestService,
private rdb: RemoteDataBuildService, private rdb: RemoteDataBuildService,
private halService: HALEndpointService) { private halService: HALEndpointService) {
@@ -51,12 +50,8 @@ export class RegistryService {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe( const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href)) getResponseFromEntry(),
);
const rmrObs: Observable<RegistryMetadataschemasResponse> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
); );
@@ -64,8 +59,8 @@ export class RegistryService {
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas) map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas)
); );
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo) map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo)
); );
@@ -75,7 +70,7 @@ export class RegistryService {
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
public getMetadataSchemaByName(schemaName: string): Observable<RemoteData<MetadataSchema>> { public getMetadataSchemaByName(schemaName: string): Observable<RemoteData<MetadataSchema>> {
@@ -90,12 +85,8 @@ export class RegistryService {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe( const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href)) getResponseFromEntry(),
);
const rmrObs: Observable<RegistryMetadataschemasResponse> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
); );
@@ -104,7 +95,7 @@ export class RegistryService {
map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0]) map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0])
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, metadataschemaObs); return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs);
} }
public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> { public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
@@ -114,12 +105,8 @@ export class RegistryService {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe( const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href)) getResponseFromEntry(),
);
const rmrObs: Observable<RegistryMetadatafieldsResponse> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
); );
@@ -128,8 +115,9 @@ export class RegistryService {
map((metadataFields: MetadataField[]) => metadataFields.filter((field) => field.schema.id === schema.id)) map((metadataFields: MetadataField[]) => metadataFields.filter((field) => field.schema.id === schema.id))
); );
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
); );
@@ -139,7 +127,7 @@ export class RegistryService {
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
public getBitstreamFormats(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<BitstreamFormat>>> { public getBitstreamFormats(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<BitstreamFormat>>> {
@@ -149,12 +137,8 @@ export class RegistryService {
flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
); );
const responseCacheObs = requestObs.pipe( const rbrObs: Observable<RegistryBitstreamformatsResponse> = requestEntryObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href)) getResponseFromEntry(),
);
const rbrObs: Observable<RegistryBitstreamformatsResponse> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse) map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse)
); );
@@ -162,8 +146,8 @@ export class RegistryService {
map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats) map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats)
); );
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo) map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo)
); );
@@ -173,7 +157,7 @@ export class RegistryService {
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
} }
private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> { private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {

View File

@@ -1,14 +1,12 @@
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service'; import { HALEndpointService } from './hal-endpoint.service';
import { EndpointMapRequest } from '../data/request.models'; import { EndpointMapRequest } from '../data/request.models';
describe('HALEndpointService', () => { describe('HALEndpointService', () => {
let service: HALEndpointService; let service: HALEndpointService;
let responseCache: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let envConfig: GlobalConfig; let envConfig: GlobalConfig;
@@ -19,14 +17,6 @@ describe('HALEndpointService', () => {
describe('getRootEndpointMap', () => { describe('getRootEndpointMap', () => {
beforeEach(() => { beforeEach(() => {
responseCache = jasmine.createSpyObj('responseCache', {
get: hot('a-', {
a: {
response: { endpointMap: endpointMap }
}
})
});
requestService = getMockRequestService(); requestService = getMockRequestService();
envConfig = { envConfig = {
@@ -34,7 +24,6 @@ describe('HALEndpointService', () => {
} as any; } as any;
service = new HALEndpointService( service = new HALEndpointService(
responseCache,
requestService, requestService,
envConfig envConfig
); );
@@ -60,12 +49,6 @@ describe('HALEndpointService', () => {
envConfig = { envConfig = {
rest: { baseUrl: 'https://rest.api/' } rest: { baseUrl: 'https://rest.api/' }
} as any; } as any;
service = new HALEndpointService(
responseCache,
requestService,
envConfig
);
}); });
it('should return the endpoint URL for the service\'s linkPath', () => { it('should return the endpoint URL for the service\'s linkPath', () => {
@@ -89,7 +72,6 @@ describe('HALEndpointService', () => {
describe('isEnabledOnRestApi', () => { describe('isEnabledOnRestApi', () => {
beforeEach(() => { beforeEach(() => {
service = new HALEndpointService( service = new HALEndpointService(
responseCache,
requestService, requestService,
envConfig envConfig
); );

View File

@@ -1,21 +1,27 @@
import {of as observableOf, Observable } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import {filter, distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; import {
distinctUntilChanged,
filter,
flatMap,
map,
startWith,
switchMap,
tap
} from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models';
import { EndpointMapRequest } from '../data/request.models'; import { EndpointMapRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response.models';
import { getResponseFromEntry } from './operators';
@Injectable() @Injectable()
export class HALEndpointService { export class HALEndpointService {
constructor(private responseCache: ResponseCacheService, constructor(private requestService: RequestService,
private requestService: RequestService,
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
} }
@@ -29,12 +35,22 @@ export class HALEndpointService {
private getEndpointMapAt(href): Observable<EndpointMap> { private getEndpointMapAt(href): Observable<EndpointMap> {
const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); const request = new EndpointMapRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
return this.responseCache.get(request.href).pipe( this.requestService.getByUUID(request.uuid).pipe(
map((entry: ResponseCacheEntry) => entry.response), getResponseFromEntry(),
filter((response: EndpointMapSuccessResponse) => isNotEmpty(response)),
map((response: EndpointMapSuccessResponse) => response.endpointMap), map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged(),); distinctUntilChanged()).subscribe((t) => console.log('uuid', t));
this.requestService.getByHref(request.href).pipe(
getResponseFromEntry(),
map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged()).subscribe((t) => console.log('href', t));
this.requestService.configure(request);
return this.requestService.getByHref(request.href).pipe( /*<-- changing this to UUID breaks it */
getResponseFromEntry(),
map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged());
} }
public getEndpoint(linkPath: string): Observable<string> { public getEndpoint(linkPath: string): Observable<string> {
@@ -48,7 +64,7 @@ export class HALEndpointService {
let currentPath; let currentPath;
const pipeArguments = path const pipeArguments = path
.map((subPath: string, index: number) => [ .map((subPath: string, index: number) => [
flatMap((href: string) => this.getEndpointMapAt(href)), switchMap((href: string) => this.getEndpointMapAt(href)),
map((endpointMap: EndpointMap) => { map((endpointMap: EndpointMap) => {
if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) { if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) {
currentPath = endpointMap[subPath]; currentPath = endpointMap[subPath];

View File

@@ -1,17 +1,15 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { GetRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { GetRequest, RestRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer'; import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { import {
configureRequest, configureRequest,
filterSuccessfulResponses, getRemoteDataPayload, filterSuccessfulResponses,
getRequestFromSelflink, getResourceLinksFromResponse, getRemoteDataPayload,
getResponseFromSelflink getRequestFromSelflink,
getResourceLinksFromResponse,
} from './operators'; } from './operators';
describe('Core Module - RxJS Operators', () => { describe('Core Module - RxJS Operators', () => {
@@ -64,44 +62,6 @@ describe('Core Module - RxJS Operators', () => {
}); });
}); });
describe('getResponseFromSelflink', () => {
let responseCacheService: ResponseCacheService;
beforeEach(() => {
scheduler = getTestScheduler();
});
it('should return the ResponseCacheEntry corresponding to the self link in the source', () => {
responseCacheService = getMockResponseCacheService();
const source = hot('a', { a: testSelfLink });
const result = source.pipe(getResponseFromSelflink(responseCacheService));
const expected = cold('a', { a: new ResponseCacheEntry()});
expect(result).toBeObservable(expected)
});
it('should use the responseCacheService to fetch the response by the request\'s link', () => {
responseCacheService = getMockResponseCacheService();
const source = hot('a', { a: testSelfLink });
scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe());
scheduler.flush();
expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink)
});
it('shouldn\'t return anything if there is no response matching the request\'s link', () => {
responseCacheService = getMockResponseCacheService(undefined, cold('a', { a: undefined }));
const source = hot('a', { a: testSelfLink });
const result = source.pipe(getResponseFromSelflink(responseCacheService));
const expected = cold('-');
expect(result).toBeObservable(expected)
});
});
describe('filterSuccessfulResponses', () => { describe('filterSuccessfulResponses', () => {
it('should only return responses for which isSuccessful === true', () => { it('should only return responses for which isSuccessful === true', () => {
const source = hot('abcde', testRCEs); const source = hot('abcde', testRCEs);

View File

@@ -1,9 +1,7 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, first, flatMap, map, tap } from 'rxjs/operators'; import { filter, first, flatMap, map, tap } from 'rxjs/operators';
import { hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { DSOSuccessResponse } from '../cache/response-cache.models'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { RestRequest } from '../data/request.models'; import { RestRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer'; import { RequestEntry } from '../data/request.reducer';
@@ -24,22 +22,25 @@ export const getRequestFromSelflink = (requestService: RequestService) =>
hasValueOperator() hasValueOperator()
); );
export const getResponseFromSelflink = (responseCache: ResponseCacheService) => export const filterSuccessfulResponses = () =>
(source: Observable<string>): Observable<ResponseCacheEntry> => (source: Observable<RequestEntry>): Observable<RestResponse> =>
source.pipe( source.pipe(
flatMap((href: string) => responseCache.get(href)), getResponseFromEntry(),
hasValueOperator() filter((response: RestResponse) => response.isSuccessful === true),
); );
export const filterSuccessfulResponses = () => export const getResponseFromEntry = () =>
(source: Observable<ResponseCacheEntry>): Observable<ResponseCacheEntry> => (source: Observable<RequestEntry>): Observable<RestResponse> =>
source.pipe(filter((entry: ResponseCacheEntry) => entry.response.isSuccessful === true)); source.pipe(
filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)),
map((entry: RequestEntry) => entry.response)
);
export const getResourceLinksFromResponse = () => export const getResourceLinksFromResponse = () =>
(source: Observable<ResponseCacheEntry>): Observable<string[]> => (source: Observable<RequestEntry>): Observable<string[]> =>
source.pipe( source.pipe(
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks), map((response: DSOSuccessResponse) => response.resourceSelfLinks),
); );
export const configureRequest = (requestService: RequestService) => export const configureRequest = (requestService: RequestService) =>
@@ -60,7 +61,7 @@ export const toDSpaceObjectListRD = () =>
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => { map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject); const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>; const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
return Object.assign(rd, {payload: payload}); return Object.assign(rd, { payload: payload });
}) })
); );

View File

@@ -1,14 +1,13 @@
import {of as observableOf, Observable } from 'rxjs'; import {of as observableOf, Observable } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { RequestEntry } from '../../core/data/request.reducer'; import { RequestEntry } from '../../core/data/request.reducer';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService { export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
return { return {
toRemoteDataObservable: (requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<any>) => { toRemoteDataObservable: (requestEntry$: Observable<RequestEntry>, payload$: Observable<any>) => {
if (hasValue(toRemoteDataObservable$)) { if (hasValue(toRemoteDataObservable$)) {
return toRemoteDataObservable$; return toRemoteDataObservable$;

View File

@@ -1,16 +0,0 @@
import {of as observableOf, Observable } from 'rxjs';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
export function getMockResponseCacheService(
add$: Observable<ResponseCacheEntry> = observableOf(new ResponseCacheEntry()),
get$: Observable<ResponseCacheEntry> = observableOf(new ResponseCacheEntry()),
has: boolean = false
): ResponseCacheService {
return jasmine.createSpyObj('ResponseCacheService', {
add: add$,
get: get$,
has,
});
}

View File

@@ -53,6 +53,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) {
function ngApp(req, res) { function ngApp(req, res) {
function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { function onHandleError(parentZoneDelegate, currentZone, targetZone, error) {
console.error('Error:', error);
console.warn('Error in SSR, serving for direct CSR'); console.warn('Error in SSR, serving for direct CSR');
res.sendFile('index.csr.html', { root: './src' }); res.sendFile('index.csr.html', { root: './src' });
} }