diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 69680dbcb..32a9203bb 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/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(); diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index a246013f1..725d46c52 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/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, query?: string) { diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 8c98a1f7e..374f57d67 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/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'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index d219c8157..31aaade4f 100644 --- a/src/Squidex/app/framework/module.ts +++ b/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 ] diff --git a/src/Squidex/app/framework/services/local-cache.service.spec.ts b/src/Squidex/app/framework/services/local-cache.service.spec.ts new file mode 100644 index 000000000..3040c49e6 --- /dev/null +++ b/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(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/services/local-cache.service.ts b/src/Squidex/app/framework/services/local-cache.service.ts new file mode 100644 index 000000000..f79e88102 --- /dev/null +++ b/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(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 entry.value; + } + } + + return undefined; + } + + public set(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; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/services/routing-cache.service.ts b/src/Squidex/app/framework/services/routing-cache.service.ts deleted file mode 100644 index ead7b1cfa..000000000 --- a/src/Squidex/app/framework/services/routing-cache.service.ts +++ /dev/null @@ -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(key: string) { - let entry = this.entries[key]; - - if (entry && (new Date().getTime() - entry.inserted) < 100) { - return entry.value; - } else { - return undefined; - } - } - - public set(key: string, value: T) { - this.entries[key] = { value, inserted: new Date().getTime() }; - } -} \ No newline at end of file diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts index 73fee408f..63a8003fc 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts +++ b/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; - let routingCache: IMock; 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, new RouterMockup(), routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).toThrow('Route must contain schema name.'); }); it('should throw if route does not contain content id', () => { - const guard = new ResolveContentGuard(appsStore.object, new RouterMockup(), routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, new RouterMockup()); expect(() => guard.resolve({ params: { appName: 'my-app', schemaName: 'my-schema' } }, {})).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, new RouterMockup(), routingCache.object); - - guard.resolve(route, {}) - .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, router, routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -94,7 +75,7 @@ describe('ResolveContentGuard', () => { .returns(() => Observable.throw(null!)); const router = new RouterMockup(); - const guard = new ResolveContentGuard(appsStore.object, router, routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -112,7 +93,7 @@ describe('ResolveContentGuard', () => { .returns(() => Observable.of(content)); const router = new RouterMockup(); - const guard = new ResolveContentGuard(appsStore.object, router, routingCache.object); + const guard = new ResolveContentGuard(appsStore.object, router); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.ts b/src/Squidex/app/shared/guards/resolve-content.guard.ts index 4b8b4b47f..d2e9efdfd 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.ts +++ b/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 { 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 { throw 'Route must contain content id.'; } - const content = this.routingCache.getValue(`content.${contentId}`); - - if (content) { - return Observable.of(content); - } - const result = this.contentsService.getContent(appName, schemaName, contentId) .do(dto => { diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts index 5d1bc2978..4e68ad839 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts +++ b/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; - let routingCache: IMock; 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, new RouterMockup(), routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup()); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, new RouterMockup()); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).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, new RouterMockup(), routingCache.object); - - guard.resolve(route, {}) - .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, router, routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -82,7 +64,7 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.throw(null)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -100,7 +82,7 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -118,7 +100,7 @@ describe('ResolvePublishedSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolvePublishedSchemaGuard(schemasService.object, router, routingCache.object); + const guard = new ResolvePublishedSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts index ad94f8ac8..c71a828b7 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts +++ b/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 { 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 { throw 'Route must contain schema name.'; } - const schema = this.routingCache.getValue(`schema.${schemaName}`); - - if (schema && schema.isPublished) { - return Observable.of(schema); - } - const result = this.schemasService.getSchema(appName, schemaName) .do(dto => { diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts index 2628b7f58..2abb64e09 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts +++ b/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; - let routingCache: IMock; 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, new RouterMockup(), routingCache.object); + const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup()); expect(() => guard.resolve({ params: {} }, {})).toThrow('Route must contain app name.'); }); it('should throw if route does not contain schema name', () => { - const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup(), routingCache.object); + const guard = new ResolveSchemaGuard(schemasService.object, new RouterMockup()); expect(() => guard.resolve({ params: { appName: 'my-app' } }, {})).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, new RouterMockup(), routingCache.object); - - guard.resolve(route, {}) - .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, router, routingCache.object); + const guard = new ResolveSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -82,7 +64,7 @@ describe('ResolveSchemaGuard', () => { .returns(() => Observable.throw(null!)); const router = new RouterMockup(); - const guard = new ResolveSchemaGuard(schemasService.object, router, routingCache.object); + const guard = new ResolveSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { @@ -100,7 +82,7 @@ describe('ResolveSchemaGuard', () => { .returns(() => Observable.of(schema)); const router = new RouterMockup(); - const guard = new ResolveSchemaGuard(schemasService.object, router, routingCache.object); + const guard = new ResolveSchemaGuard(schemasService.object, router); guard.resolve(route, {}) .subscribe(result => { diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts index 95e494cc4..093bd45e2 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.ts +++ b/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 { 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 { throw 'Route must contain schema name.'; } - const schema = this.routingCache.getValue(`schema.${schemaName}`); - - if (schema) { - return Observable.of(schema); - } - const result = this.schemasService.getSchema(appName, schemaName) .do(dto => { diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts index d5c5f84a9..f9590f77c 100644 --- a/src/Squidex/app/shared/services/apps-store.service.ts +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -61,11 +61,6 @@ export class AppsStoreService { public createApp(dto: CreateAppDto, now?: DateTime): Observable { 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])); diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index f1d721d10..fb8e225c1 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/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) => { diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index a11fb7e8f..016388641 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/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 { - return new Observable(subscriber => { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`); + const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets`); - const req = new HttpRequest('POST', url, getFormData(file), { - reportProgress: true - }); + 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 { @@ -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.'); } diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 973a2d19f..4c3c2db15 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/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) => { diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index c79014d45..21460b736 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/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.'); } diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 85c082efa..61c9e4173 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/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) => { diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 0a13903c9..1c1307442 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/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.'); }