Browse Source

Caching improved again.

pull/95/head
Sebastian Stehle 8 years ago
parent
commit
113844b05f
  1. 6
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  2. 10
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  3. 2
      src/Squidex/app/framework/declarations.ts
  4. 4
      src/Squidex/app/framework/module.ts
  5. 53
      src/Squidex/app/framework/services/local-cache.service.spec.ts
  6. 54
      src/Squidex/app/framework/services/local-cache.service.ts
  7. 24
      src/Squidex/app/framework/services/routing-cache.service.ts
  8. 33
      src/Squidex/app/shared/guards/resolve-content.guard.spec.ts
  9. 11
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  10. 36
      src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts
  11. 11
      src/Squidex/app/shared/guards/resolve-published-schema.guard.ts
  12. 34
      src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts
  13. 11
      src/Squidex/app/shared/guards/resolve-schema.guard.ts
  14. 5
      src/Squidex/app/shared/services/apps-store.service.ts
  15. 31
      src/Squidex/app/shared/services/assets.service.spec.ts
  16. 44
      src/Squidex/app/shared/services/assets.service.ts
  17. 29
      src/Squidex/app/shared/services/contents.service.spec.ts
  18. 20
      src/Squidex/app/shared/services/contents.service.ts
  19. 27
      src/Squidex/app/shared/services/schemas.service.spec.ts
  20. 21
      src/Squidex/app/shared/services/schemas.service.ts

6
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -29,7 +29,6 @@ import {
MessageBus,
NotificationService,
Pager,
RoutingCache,
SchemaDetailsDto
} from 'shared';
@ -62,7 +61,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
private readonly authService: AuthService,
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute,
private readonly routingCache: RoutingCache,
private readonly messageBus: MessageBus
) {
super(notifications, apps);
@ -170,10 +168,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.languageSelected = language;
}
public cacheContent(content: ContentDto) {
this.routingCache.set(`content.${content.id}`, content);
}
public goNext() {
this.contentsPager = this.contentsPager.goNext();

10
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts

@ -6,7 +6,7 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
@ -18,7 +18,6 @@ import {
MessageBus,
ModalView,
NotificationService,
RoutingCache,
SchemaDto,
SchemasService
} from 'shared';
@ -47,9 +46,7 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy,
constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly schemasService: SchemasService,
private readonly messageBus: MessageBus,
private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly routingCache: RoutingCache
private readonly route: ActivatedRoute
) {
super(notifications, apps);
}
@ -103,9 +100,6 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy,
this.updateSchemas(this.schemas.push(dto), this.schemaQuery);
this.addSchemaDialog.hide();
this.routingCache.set(`schema.${dto.name}`, dto);
this.router.navigate([ dto.name ], { relativeTo: this.route });
}
private updateSchemas(schemas: ImmutableArray<SchemaDto>, query?: string) {

2
src/Squidex/app/framework/declarations.ts

@ -50,11 +50,11 @@ export * from './configurations';
export * from './services/clipboard.service';
export * from './services/local-store.service';
export * from './services/local-cache.service';
export * from './services/message-bus';
export * from './services/notification.service';
export * from './services/resource-loader.service';
export * from './services/root-view.service';
export * from './services/routing-cache.service';
export * from './services/shortcut.service';
export * from './services/title.service';

4
src/Squidex/app/framework/module.ts

@ -32,6 +32,7 @@ import {
IndeterminateValueDirective,
JsonEditorComponent,
KNumberPipe,
LocalCacheService,
LocalStoreService,
LowerCaseInputDirective,
MarkdownEditorComponent,
@ -50,7 +51,6 @@ import {
RichEditorComponent,
RootViewDirective,
RootViewService,
RoutingCache,
ScrollActiveDirective,
ShortcutComponent,
ShortcutService,
@ -178,12 +178,12 @@ export class SqxFrameworkModule {
providers: [
CanDeactivateGuard,
ClipboardService,
LocalCacheService,
LocalStoreService,
MessageBus,
NotificationService,
ResourceLoaderService,
RootViewService,
RoutingCache,
ShortcutService,
TitleService
]

53
src/Squidex/app/framework/services/local-cache.service.spec.ts

@ -0,0 +1,53 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { LocalCacheService, LocalCacheServiceFactory } from './../';
describe('LocalCache', () => {
it('should instantiate from factory', () => {
const localCacheService = LocalCacheServiceFactory();
expect(localCacheService).toBeDefined();
});
it('should instantiate', () => {
const localCacheService = new LocalCacheService();
expect(localCacheService).toBeDefined();
});
it('should get and store item in cache', () => {
const localCacheService = new LocalCacheService();
const value = {};
localCacheService.set('key', value);
expect(localCacheService.get('key')).toBe(value);
});
it('should get and store item in cache', () => {
const localCacheService = new LocalCacheService();
const value = {};
localCacheService.set('key', value);
localCacheService.clear(true);
expect(localCacheService.get('key')).toBeUndefined();
});
it('should not retrieve item if expired', () => {
const localCacheService = new LocalCacheService();
const value = {};
localCacheService.set('key', value);
expect(localCacheService.get('key', new Date().getTime() + 400)).toBeUndefined();
});
});

54
src/Squidex/app/framework/services/local-cache.service.ts

@ -0,0 +1,54 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
interface Entry { value: any, expires: number };
export const LocalCacheServiceFactory = () => {
return new LocalCacheService();
};
export class LocalCacheService {
private readonly entries: { [key: string]: Entry } = {};
public clear(force: boolean) {
const now = new Date().getTime();
for (let key in this.entries) {
if (this.entries.hasOwnProperty(key)) {
const entry = this.entries[key];
if (force || LocalCacheService.isExpired(now, entry)) {
delete this.entries[key];
}
}
}
}
public get<T>(key: string, now?: number): T {
const entry = this.entries[key];
if (entry) {
now = now || new Date().getTime();
if (!LocalCacheService.isExpired(now, entry)) {
delete this.entries[key];
return <T> entry.value;
}
}
return undefined;
}
public set<T>(key: string, value: T, expiresIn = 100) {
this.entries[key] = { value, expires: new Date().getTime() + expiresIn };
}
private static isExpired(now: number, entry: Entry): boolean {
return entry.expires < now;
}
}

24
src/Squidex/app/framework/services/routing-cache.service.ts

@ -1,24 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class RoutingCache {
private readonly entries: { [key: string]: { value: any, inserted: number } } = {};
public getValue<T>(key: string) {
let entry = this.entries[key];
if (entry && (new Date().getTime() - entry.inserted) < 100) {
return <T> entry.value;
} else {
return undefined;
}
}
public set<T>(key: string, value: T) {
this.entries[key] = { value, inserted: new Date().getTime() };
}
}

33
src/Squidex/app/shared/guards/resolve-content.guard.spec.ts

@ -8,7 +8,7 @@
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { ContentsService, RoutingCache } from 'shared';
import { ContentsService } from 'shared';
import { ResolveContentGuard } from './resolve-content.guard';
import { RouterMockup } from './router-mockup';
@ -31,54 +31,35 @@ describe('ResolveContentGuard', () => {
};
let appsStore: IMock<ContentsService>;
let routingCache: IMock<RoutingCache>;
beforeEach(() => {
appsStore = Mock.ofType(ContentsService);
routingCache = Mock.ofType(RoutingCache);
});
it('should throw if route does not contain app name', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app name.');
});
it('should throw if route does not contain schema name', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: { appName: 'my-app' } }, <any>{})).toThrow('Route must contain schema name.');
});
it('should throw if route does not contain content id', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: { appName: 'my-app', schemaName: 'my-schema' } }, <any>{})).toThrow('Route must contain content id.');
});
it('should provide content from cache if found', (done) => {
const content = { };
routingCache.setup(x => x.getValue('content.123'))
.returns(() => content);
const guard = new ResolveContentGuard(appsStore.object, <any>new RouterMockup(), routingCache.object);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBe(content);
done();
});
});
it('should navigate to 404 page if schema is not found', (done) => {
appsStore.setup(x => x.getContent('my-app', 'my-schema', '123'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router, routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -94,7 +75,7 @@ describe('ResolveContentGuard', () => {
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router, routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -112,7 +93,7 @@ describe('ResolveContentGuard', () => {
.returns(() => Observable.of(content));
const router = new RouterMockup();
const guard = new ResolveContentGuard(appsStore.object, <any>router, routingCache.object);
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {

11
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams, RoutingCache } from 'framework';
import { allParams } from 'framework';
import { ContentDto, ContentsService } from './../services/contents.service';
@ -17,8 +17,7 @@ import { ContentDto, ContentsService } from './../services/contents.service';
export class ResolveContentGuard implements Resolve<ContentDto> {
constructor(
private readonly contentsService: ContentsService,
private readonly router: Router,
private readonly routingCache: RoutingCache
private readonly router: Router
) {
}
@ -43,12 +42,6 @@ export class ResolveContentGuard implements Resolve<ContentDto> {
throw 'Route must contain content id.';
}
const content = this.routingCache.getValue<ContentDto>(`content.${contentId}`);
if (content) {
return Observable.of(content);
}
const result =
this.contentsService.getContent(appName, schemaName, contentId)
.do(dto => {

36
src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts

@ -8,9 +8,10 @@
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { RoutingCache, SchemasService } from 'shared';
import { RouterMockup } from './router-mockup';
import { SchemasService } from 'shared';
import { ResolvePublishedSchemaGuard } from './resolve-published-schema.guard';
import { RouterMockup } from './router-mockup';
describe('ResolvePublishedSchemaGuard', () => {
const route = {
@ -25,48 +26,29 @@ describe('ResolvePublishedSchemaGuard', () => {
};
let schemasService: IMock<SchemasService>;
let routingCache: IMock<RoutingCache>;
beforeEach(() => {
schemasService = Mock.ofType(SchemasService);
routingCache = Mock.ofType(RoutingCache);
});
it('should throw if route does not contain app name', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app name.');
});
it('should throw if route does not contain schema name', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: { appName: 'my-app' } }, <any>{})).toThrow('Route must contain schema name.');
});
it('should provide schema from cache if found', (done) => {
const schema = { isPublished: true };
routingCache.setup(x => x.getValue('schema.my-schema'))
.returns(() => schema);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBe(schema);
done();
});
});
it('should navigate to 404 page if schema is not found', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -82,7 +64,7 @@ describe('ResolvePublishedSchemaGuard', () => {
.returns(() => Observable.throw(null));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -100,7 +82,7 @@ describe('ResolvePublishedSchemaGuard', () => {
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -118,7 +100,7 @@ describe('ResolvePublishedSchemaGuard', () => {
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {

11
src/Squidex/app/shared/guards/resolve-published-schema.guard.ts

@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams, RoutingCache } from 'framework';
import { allParams } from 'framework';
import { SchemaDetailsDto, SchemasService } from './../services/schemas.service';
@ -17,8 +17,7 @@ import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'
export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
constructor(
private readonly schemasService: SchemasService,
private readonly router: Router,
private readonly routingCache: RoutingCache
private readonly router: Router
) {
}
@ -37,12 +36,6 @@ export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
throw 'Route must contain schema name.';
}
const schema = this.routingCache.getValue<SchemaDetailsDto>(`schema.${schemaName}`);
if (schema && schema.isPublished) {
return Observable.of(schema);
}
const result =
this.schemasService.getSchema(appName, schemaName)
.do(dto => {

34
src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts

@ -8,9 +8,10 @@
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { RoutingCache, SchemasService } from 'shared';
import { RouterMockup } from './router-mockup';
import { SchemasService } from 'shared';
import { ResolveSchemaGuard } from './resolve-schema.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveSchemaGuard', () => {
const route = {
@ -25,48 +26,29 @@ describe('ResolveSchemaGuard', () => {
};
let schemasService: IMock<SchemasService>;
let routingCache: IMock<RoutingCache>;
beforeEach(() => {
schemasService = Mock.ofType(SchemasService);
routingCache = Mock.ofType(RoutingCache);
});
it('should throw if route does not contain app name', () => {
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app name.');
});
it('should throw if route does not contain schema name', () => {
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: { appName: 'my-app' } }, <any>{})).toThrow('Route must contain schema name.');
});
it('should provide schema from cache if found', (done) => {
const schema = { isPublished: true };
routingCache.setup(x => x.getValue('schema.my-schema'))
.returns(() => schema);
const guard = new ResolveSchemaGuard(schemasService.object, <any>new RouterMockup(), routingCache.object);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBe(schema);
done();
});
});
it('should navigate to 404 page if schema is not found', (done) => {
schemasService.setup(x => x.getSchema('my-app', 'my-schema'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -82,7 +64,7 @@ describe('ResolveSchemaGuard', () => {
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
@ -100,7 +82,7 @@ describe('ResolveSchemaGuard', () => {
.returns(() => Observable.of(schema));
const router = new RouterMockup();
const guard = new ResolveSchemaGuard(schemasService.object, <any>router, routingCache.object);
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {

11
src/Squidex/app/shared/guards/resolve-schema.guard.ts

@ -9,7 +9,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams, RoutingCache } from 'framework';
import { allParams } from 'framework';
import { SchemaDetailsDto, SchemasService } from './../services/schemas.service';
@ -17,8 +17,7 @@ import { SchemaDetailsDto, SchemasService } from './../services/schemas.service'
export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
constructor(
private readonly schemasService: SchemasService,
private readonly router: Router,
private readonly routingCache: RoutingCache
private readonly router: Router
) {
}
@ -37,12 +36,6 @@ export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
throw 'Route must contain schema name.';
}
const schema = this.routingCache.getValue<SchemaDetailsDto>(`schema.${schemaName}`);
if (schema) {
return Observable.of(schema);
}
const result =
this.schemasService.getSchema(appName, schemaName)
.do(dto => {

5
src/Squidex/app/shared/services/apps-store.service.ts

@ -61,11 +61,6 @@ export class AppsStoreService {
public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
return this.appsService.postApp(dto)
.map(created => {
now = now || DateTime.now();
return new AppDto(created.id, dto.name, 'Owner', now, now);
})
.do(app => {
this.apps$.first().subscribe(apps => {
this.apps$.next(apps.concat([app]));

31
src/Squidex/app/shared/services/assets.service.spec.ts

@ -15,6 +15,7 @@ import {
AssetReplacedDto,
AssetsService,
DateTime,
LocalCacheService,
UpdateAssetDto,
Version
} from './../';
@ -62,6 +63,7 @@ describe('AssetsService', () => {
],
providers: [
AssetsService,
LocalCacheService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
@ -156,10 +158,10 @@ describe('AssetsService', () => {
it('should make get request to get asset',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let assets: AssetDto | null = null;
let asset: AssetDto | null = null;
assetsService.getAsset('my-app', '123').subscribe(result => {
assets = result;
asset = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123');
@ -184,7 +186,7 @@ describe('AssetsService', () => {
version: 11
});
expect(assets).toEqual(
expect(asset).toEqual(
new AssetDto(
'id1', 'Created1', 'LastModifiedBy1',
DateTime.parseISO_UTC('2016-12-12T10:10'),
@ -200,6 +202,29 @@ describe('AssetsService', () => {
new Version('11')));
}));
it('should provide entry from cache if not found',
inject([LocalCacheService, AssetsService, HttpTestingController], (localCache: LocalCacheService, assetsService: AssetsService, httpMock: HttpTestingController) => {
const cached = {};
localCache.set('asset.123', cached, 10000);
let asset: AssetDto | null = null;
assetsService.getAsset('my-app', '123').subscribe(result => {
asset = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({}, { status: 404, statusText: '404' });
expect(asset).toBe(cached);
}));
it('should append query to find by name',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {

44
src/Squidex/app/shared/services/assets.service.ts

@ -5,13 +5,14 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient, HttpEventType, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { HttpClient, HttpErrorResponse, HttpEventType, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {
ApiUrlConfig,
DateTime,
LocalCacheService,
HTTP,
Version
} from 'framework';
@ -100,7 +101,8 @@ export class AssetReplacedDto {
export class AssetsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
private readonly apiUrl: ApiUrlConfig,
private readonly localCache: LocalCacheService
) {
}
@ -152,22 +154,20 @@ export class AssetsService {
}
public uploadFile(appName: string, file: File, user: string, now?: DateTime): Observable<number | AssetDto> {
return new Observable<number | AssetDto>(subscriber => {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`);
const req = new HttpRequest('POST', url, getFormData(file), {
reportProgress: true
});
this.http.request(req)
.pretifyError('Failed to upload asset. Please reload.')
.subscribe(event => {
return this.http.request(req)
.map(event => {
if (event.type === HttpEventType.UploadProgress) {
const percentDone = Math.round(100 * event.loaded / event.total);
subscriber.next(percentDone);
return percentDone;
} else if (event instanceof HttpResponse) {
const response = event.body;
const response: any = event.body;
now = now || DateTime.now();
@ -187,14 +187,17 @@ export class AssetsService {
response.pixelHeight,
new Version(response.version.toString()));
subscriber.next(dto);
this.localCache.set(`asset.${dto.id}`, dto, 5000);
return dto;
}
}, err => {
subscriber.error(err);
}, () => {
subscriber.complete();
});
});
})
.do(dto => {
if (dto instanceof AssetDto) {
this.localCache.set(`asset.${dto.id}`, dto, 5000);
}
})
.pretifyError('Failed to upload asset. Please reload.');
}
public getAsset(appName: string, id: string, version?: Version): Observable<AssetDto> {
@ -218,6 +221,17 @@ export class AssetsService {
response.pixelHeight,
new Version(response.version.toString()));
})
.catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) {
const cached = this.localCache.get(`asset.${id}`);
if (cached) {
return Observable.of(cached);
}
}
return Observable.throw(error);
})
.pretifyError('Failed to load assets. Please reload.');
}

29
src/Squidex/app/shared/services/contents.service.spec.ts

@ -14,6 +14,7 @@ import {
ContentsDto,
ContentsService,
DateTime,
LocalCacheService,
Version
} from './../';
@ -62,6 +63,7 @@ describe('ContentsService', () => {
],
providers: [
ContentsService,
LocalCacheService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
@ -182,11 +184,11 @@ describe('ContentsService', () => {
let content: ContentDto | null = null;
contentsService.getContent('my-app', 'my-schema', 'content1', version).subscribe(result => {
contentsService.getContent('my-app', 'my-schema', '1', version).subscribe(result => {
content = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1');
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value);
@ -210,6 +212,29 @@ describe('ContentsService', () => {
new Version('11')));
}));
it('should provide entry from cache if not found',
inject([LocalCacheService, ContentsService, HttpTestingController], (localCache: LocalCacheService, contentsService: ContentsService, httpMock: HttpTestingController) => {
const cached = {};
localCache.set('content.1', cached, 10000);
let content: ContentDto | null = null;
contentsService.getContent('my-app', 'my-schema', '1', version).subscribe(result => {
content = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/1');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBe(version.value);
req.flush({}, { status: 404, statusText: '404' });
expect(content).toBe(cached);
}));
it('should make post request to create content',
inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {

20
src/Squidex/app/shared/services/contents.service.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@ -14,6 +14,7 @@ import 'framework/angular/http-extensions';
import {
ApiUrlConfig,
DateTime,
LocalCacheService,
HTTP,
Version
} from 'framework';
@ -74,7 +75,8 @@ export class ContentDto {
export class ContentsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
private readonly apiUrl: ApiUrlConfig,
private readonly localCache: LocalCacheService
) {
}
@ -141,6 +143,17 @@ export class ContentsService {
response.data,
new Version(response.version.toString()));
})
.catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) {
const cached = this.localCache.get(`content.${id}`);
if (cached) {
return Observable.of(cached);
}
}
return Observable.throw(error);
})
.pretifyError('Failed to load content. Please reload.');
}
@ -159,6 +172,9 @@ export class ContentsService {
response.data,
new Version(response.version.toString()));
})
.do(content => {
this.localCache.set(`content.${content.id}`, content, 5000);
})
.pretifyError('Failed to create content. Please reload.');
}

27
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -15,6 +15,7 @@ import {
createProperties,
DateTime,
FieldDto,
LocalCacheService,
SchemaDetailsDto,
SchemaDto,
SchemaPropertiesDto,
@ -143,7 +144,7 @@ describe('SchemaDetailsDto', () => {
expect(schema_2.lastModifiedBy).toEqual('me');
});
it('should update fields property and user info when updatinmg field', () => {
it('should update fields property and user info when updating field', () => {
const field1 = new FieldDto(1, '1', false, false, 'l', createProperties('String'));
const field2_1 = new FieldDto(2, '2', false, false, 'l', createProperties('Number'));
const field2_2 = new FieldDto(2, '2', false, false, 'l', createProperties('Boolean'));
@ -170,6 +171,7 @@ describe('SchemasService', () => {
HttpClientTestingModule
],
providers: [
LocalCacheService,
SchemasService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
@ -371,6 +373,29 @@ describe('SchemasService', () => {
]));
}));
it('should provide entry from cache if not found',
inject([LocalCacheService, SchemasService, HttpTestingController], (localCache: LocalCacheService, schemasService: SchemasService, httpMock: HttpTestingController) => {
const cached = {};
localCache.set('schema.my-app.my-schema', cached, 10000);
let schema: SchemaDetailsDto | null = null;
schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => {
schema = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({}, { status: 404, statusText: '404' });
expect(schema).toBe(cached);
}));
it('should make post request to create schema',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {

21
src/Squidex/app/shared/services/schemas.service.ts

@ -5,7 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
@ -15,6 +15,7 @@ import 'framework/angular/http-extensions';
import {
ApiUrlConfig,
DateTime,
LocalCacheService,
HTTP,
ValidatorsEx,
Version
@ -614,7 +615,8 @@ export class CreateSchemaDto {
export class SchemasService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
private readonly apiUrl: ApiUrlConfig,
private readonly localCache: LocalCacheService
) {
}
@ -675,6 +677,17 @@ export class SchemasService {
new Version(response.version.toString()),
fields);
})
.catch(error => {
if (error instanceof HttpErrorResponse && error.status === 404) {
const cached = this.localCache.get(`schema.${appName}.${id}`);
if (cached) {
return Observable.of(cached);
}
}
return Observable.throw(error);
})
.pretifyError('Failed to load schema. Please reload.');
}
@ -697,6 +710,10 @@ export class SchemasService {
version,
dto.fields || []);
})
.do(schema => {
this.localCache.set(`service.${appName}.${schema.id}`, schema, 5000);
this.localCache.set(`service.${appName}.${schema.name}`, schema, 5000);
})
.pretifyError('Failed to create schema. Please reload.');
}

Loading…
Cancel
Save