RED-4632: use caching module from common-ui

This commit is contained in:
Dan Percic 2022-07-25 15:40:54 +03:00
parent d86312b38f
commit f249f3d71d
17 changed files with 36 additions and 426 deletions

View File

@ -1,18 +1,6 @@
{
"version": 1,
"projects": {
"iqser-cache": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"root": "libs/iqser-cache",
"sourceRoot": "libs/iqser-cache/src",
"prefix": "redaction",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
},
"red-domain": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",

View File

@ -13,7 +13,6 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '@environments/environment';
import { AuthModule } from './modules/auth/auth.module';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { HttpCacheInterceptor } from '@iqser/cache';
import { NotificationsComponent } from '@components/notifications/notifications.component';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
import { AppRoutingModule } from './app-routing.module';
@ -32,6 +31,7 @@ import { PruningTranslationLoader } from '@utils/pruning-translation-loader';
import { DatePipe } from '@shared/pipes/date.pipe';
import * as links from '../assets/help-mode/links.json';
import {
CachingModule,
HELP_DOCS,
IqserHelpModeModule,
MANUAL_BASE_URL,
@ -56,6 +56,7 @@ import { SystemPreferencesService } from '@services/system-preferences.service';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { LicenseService } from '@services/license.service';
import { TenantIdInterceptor } from '@utils/tenant-id-interceptor';
import { UI_CACHES } from '@utils/constants';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -86,6 +87,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
AppRoutingModule,
MonacoEditorModule,
IqserHelpModeModule,
CachingModule.forRoot(UI_CACHES),
PdfViewerModule,
ToastrModule.forRoot({
closeButton: true,
@ -174,11 +176,6 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
multi: true,
useClass: TenantIdInterceptor,
},
{
provide: HTTP_INTERCEPTORS,
multi: true,
useClass: HttpCacheInterceptor,
},
{
provide: APP_INITIALIZER,
multi: true,

View File

@ -4,16 +4,15 @@ import { Dictionary, Dossier, DOSSIER_ID, DOSSIER_TEMPLATE_ID, File, FILE_ID } f
import { Router } from '@angular/router';
import { FilesMapService } from '@services/files/files-map.service';
import { PermissionsService } from '@services/permissions.service';
import { boolFactory, getParam, LoadingService } from '@iqser/common-ui';
import { boolFactory, getParam, LoadingService, wipeCache } from '@iqser/common-ui';
import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators';
import { FileManagementService } from '@services/files/file-management.service';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { wipeFilesCache } from '@iqser/cache';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { FilesService } from '@services/files/files.service';
import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { MatDialogRef } from '@angular/material/dialog';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service';
@ -127,7 +126,7 @@ export class FilePreviewStateService {
#downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable<Blob> {
const downloadFile$ = this.#getFileToDownload(cacheIdentifier);
const obs = wipeCaches ? from(wipeFilesCache()) : of({});
const obs = wipeCaches ? from(wipeCache('files')) : of({});
return obs.pipe(switchMap(() => downloadFile$));
}

View File

@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Title } from '@angular/platform-browser';
import packageInfo from '../../../../../package.json';
import envConfig from '../../assets/config/config.json';
import { CacheApiService, wipeAllCaches } from '@iqser/cache';
import { CacheApiService, wipeAllCaches } from '@iqser/common-ui';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AppConfig } from '@red/domain';

View File

@ -2,10 +2,9 @@ import { inject, Inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import jwt_decode from 'jwt-decode';
import { ICreateUserRequest, IMyProfileUpdateRequest, IProfileUpdateRequest, IResetPasswordRequest, IUser, User } from '@red/domain';
import { wipeCaches } from '@iqser/cache';
import { BASE_HREF } from '../tokens';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { EntitiesService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { CacheApiService, EntitiesService, List, mapEach, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { tap } from 'rxjs/operators';
@Injectable({
@ -17,7 +16,11 @@ export class UserService extends EntitiesService<IUser, User> {
protected readonly _entityClass = User;
readonly #currentUser$ = new BehaviorSubject<User>(null);
constructor(@Inject(BASE_HREF) private readonly _baseHref: string, private readonly _keycloakService: KeycloakService) {
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _keycloakService: KeycloakService,
private readonly _cacheApiService: CacheApiService,
) {
super();
this.currentUser$ = this.#currentUser$.asObservable();
}
@ -46,7 +49,7 @@ export class UserService extends EntitiesService<IUser, User> {
}
logout() {
wipeCaches().then();
this._cacheApiService.wipeCaches().then();
this._keycloakService.logout().then();
}

View File

@ -1,2 +1,23 @@
import { DynamicCaches } from '@iqser/common-ui';
export const CHANGED_CHECK_INTERVAL = 5000;
export const FALLBACK_COLOR = '#CCCCCC';
export const UI_CACHES: DynamicCaches = [
{
urls: ['/assets/icons', '/assets/wv-resources'],
name: 'cached-assets',
clearOnLogout: false,
maxAge: 24 * 3600,
maxSize: 1000,
methods: ['GET'],
},
{
urls: ['/download/original'],
name: 'files',
maxAge: 3600 * 24 * 7,
maxSize: 1000,
clearOnLogout: true,
methods: ['GET'],
},
];

@ -1 +1 @@
Subproject commit cd09d7254551d0f091d49a8be22c1bdfc5b8e60b
Subproject commit 2bc66542a6032b7b54cc3a3acec9737af6618031

View File

@ -1,28 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["**/*.ts"],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "redaction",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "redaction",
"style": "kebab-case"
}
],
"max-len": "off"
}
}
]
}

View File

@ -1,7 +0,0 @@
# iqser-cache
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test iqser-cache` to execute the unit tests.

View File

@ -1,5 +0,0 @@
/*
* Public API Surface of communication-lib
*/
export * from './lib/index';

View File

@ -1,246 +0,0 @@
import { Injectable } from '@angular/core';
import { APP_LEVEL_CACHE, DYNAMIC_CACHES } from './cache-utils';
import { HttpEvent, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http';
import { from, Observable, throwError } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class CacheApiService {
constructor() {
this.checkCachesExpiration();
}
get cachesAvailable(): boolean {
return !!window.caches;
}
cacheRequest(request: HttpRequest<any>, httpResponse: HttpResponse<any>): Promise<any> {
if (httpResponse.status < 300 && httpResponse.status >= 200) {
const url = this._buildUrl(request);
for (const dynCache of DYNAMIC_CACHES) {
for (const cacheUrl of dynCache.urls) {
if (url.indexOf(cacheUrl) >= 0) {
return caches.open(dynCache.name).then(cache => this._handleFetchResponse(httpResponse, dynCache, cache, url));
}
}
}
}
}
getCachedRequest(request: HttpRequest<any>): Observable<HttpEvent<any>> {
const url = this._buildUrl(request);
return from(caches.match(url)).pipe(
mergeMap(response => {
if (response) {
const expires = response.headers.get('_expires');
if (expires) {
// if not expired, return, else override
if (parseInt(expires, 10) > new Date().getTime()) {
// console.log('[CACHE-API] Returning from cache: ', url);
return this._toHttpResponse(response);
}
// console.log('[CACHE-API] cache expired: ', url);
} else {
// console.log('[CACHE-API] Returning from cache: ', url);
return this._toHttpResponse(response);
}
}
return throwError(() => new Error('Request not Cached'));
}),
);
}
cacheValue(name: string, valueReference: any, ttl = 3600): Promise<any> {
if (this.cachesAvailable) {
return caches.open(APP_LEVEL_CACHE).then(cache => {
const string = JSON.stringify(valueReference);
const expires = new Date().getTime() + ttl * 1000;
const response = new Response(string, {
headers: {
_expires: `${expires}`,
},
});
const request = new Request(name);
// console.log('should cache', valueReference, string, response);
return cache.put(request, response);
});
}
return Promise.resolve();
}
getCachedValue(name: string): Promise<any> {
if (this.cachesAvailable) {
return caches.open(APP_LEVEL_CACHE).then(cache =>
cache.match(name).then(result => {
if (result) {
const expires = result.headers.get('_expires');
try {
if (parseInt(expires, 10) > new Date().getTime()) {
return result.json();
}
} catch (e) {}
}
return undefined;
}),
);
}
return Promise.resolve(undefined);
}
_buildUrl(request: HttpRequest<any>) {
let url: string;
if (request.method === 'GET') {
url = request.urlWithParams;
}
if (request.method === 'POST') {
const body = request.body;
let hash: string;
if (Array.isArray(body)) {
hash = JSON.stringify(body.sort());
} else {
hash = JSON.stringify(body);
}
const separator = request.urlWithParams.indexOf('?') > 0 ? '&' : '?';
url = request.urlWithParams + separator + btoa(hash);
}
return url;
}
isCachable(event: HttpRequest<any>) {
// only do shit for post and get
if (this.cachesAvailable && (event.method === 'GET' || event.method === 'POST')) {
// quick check if it has the potential of caching
const preliminaryUrl = event.url;
let tryCache = false;
for (const cache of DYNAMIC_CACHES) {
if (cache.methods.indexOf(event.method) >= 0) {
for (const url of cache.urls) {
if (preliminaryUrl.indexOf(url) >= 0) {
tryCache = true;
break;
}
}
}
}
return tryCache;
}
return false;
}
checkCachesExpiration() {
if (this.cachesAvailable) {
const now = new Date().getTime();
for (const dynCache of DYNAMIC_CACHES) {
this._handleCacheExpiration(dynCache, now);
}
}
}
private _toHttpResponse(response: Response): Observable<HttpResponse<any>> {
response = response.clone();
const contentType = response.headers.get('content-type');
let obs: Observable<any>;
if (contentType) {
if (contentType.toLowerCase().indexOf('application/json') >= 0) {
obs = from(response.json());
}
if (contentType.toLowerCase().indexOf('text/') >= 0) {
obs = from(response.text());
}
if (contentType.toLowerCase().indexOf('image/') >= 0) {
if (contentType.toLowerCase().indexOf('image/svg') >= 0) {
obs = from(response.text());
} else {
obs = from(response.blob());
}
}
if (contentType.toLowerCase().indexOf('application/pdf') >= 0) {
obs = from(response.blob());
}
}
// console.log('[CACHE-API] content type', contentType, response.url);
return obs.pipe(
map(
body =>
// console.log('[CACHE-API] BODY', body);
new HttpResponse({
body,
status: response.status,
statusText: response.statusText,
url: response.url,
headers: this._toHttpHeaders(response.headers),
}),
),
);
}
private _handleFetchResponse(httpResponse: HttpResponse<any>, dynCache, cache, url) {
const expires = new Date().getTime() + dynCache.maxAge * 1000;
const cachedResponseFields = {
status: httpResponse.status,
statusText: httpResponse.statusText,
headers: {
_expires: undefined,
},
};
httpResponse.headers.keys().forEach(key => {
cachedResponseFields.headers[key] = httpResponse.headers.get(key);
});
cachedResponseFields.headers._expires = expires;
let body;
if (cachedResponseFields.headers['content-type'].indexOf('application/json') >= 0) {
body = JSON.stringify(httpResponse.body);
} else {
body = httpResponse.body;
}
cache.put(url, new Response(body, cachedResponseFields));
}
private _handleCacheExpiration(dynCache, now) {
caches.open(dynCache.name).then(cache => {
cache.keys().then(keys => {
// removed expired;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// console.log('[CACHE-API] checking cache key: ', key);
cache.match(key).then(response => {
const expires = response.headers.get('_expires');
try {
if (parseInt(expires, 10) < now) {
cache.delete(key);
}
} catch (e) {}
});
}
});
cache.keys().then(keys => {
if (keys.length > dynCache.maxSize) {
const keysToRemove = keys.slice(0, keys.length - dynCache.maxSize);
// console.log('[CACHE-API] cache to large - removing keys: ', keysToRemove);
for (let i = 0; i < keysToRemove.length; i++) {
const key = keys[i];
cache.delete(key);
}
}
});
});
}
private _toHttpHeaders(headers: Headers): HttpHeaders {
let httpHeaders = new HttpHeaders();
headers.forEach((value, key) => {
httpHeaders = httpHeaders.append(key, value);
});
return httpHeaders;
}
}

View File

@ -1,49 +0,0 @@
export const APP_LEVEL_CACHE = 'app-level-cache';
export const DYNAMIC_CACHES = [
{
urls: ['/assets/icons', '/assets/wv-resources'],
name: 'cached-assets',
clearOnLogout: false,
maxAge: 24 * 3600,
maxSize: 1000,
methods: ['GET'],
},
{
urls: ['/download/original'],
name: 'files',
maxAge: 3600 * 24 * 7,
maxSize: 1000,
clearOnLogout: true,
methods: ['GET'],
},
];
export async function wipeAllCaches() {
const keys = await caches.keys();
for (const cache of keys) {
await caches.delete(cache);
}
}
export async function wipeCaches(logoutDependant: boolean = false) {
await caches.delete(APP_LEVEL_CACHE);
for (const cache of DYNAMIC_CACHES) {
if (!logoutDependant) {
await caches.delete(cache.name);
}
if (logoutDependant && cache.clearOnLogout) {
await caches.delete(cache.name);
}
}
}
export function wipeFilesCache() {
return caches.delete('files');
}
export async function wipeCacheEntry(cacheName: string, entry: string) {
return caches.open(cacheName).then(cache => {
cache.delete(entry, { ignoreSearch: false });
});
}

View File

@ -1,43 +0,0 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { CacheApiService } from './cache-api.service';
import { catchError, tap } from 'rxjs/operators';
@Injectable()
export class HttpCacheInterceptor implements HttpInterceptor {
constructor(private _cacheApiService: CacheApiService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// continue if not cachable.
if (!this._cacheApiService.isCachable(req)) {
return next.handle(req);
}
return this._cacheApiService.getCachedRequest(req).pipe(
tap(() => {
// console.log('[CACHE-API] got from cache', ok);
}),
catchError(() =>
// console.log("[CACHE-API] Cache fetch error", cacheError, req.url);
this.sendRequest(req, next),
),
);
}
/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
sendRequest(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// console.log('[CACHE-API] request', request.url);
return next.handle(request).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
this._cacheApiService.cacheRequest(request, event);
}
}),
);
}
}

View File

@ -1,3 +0,0 @@
export * from './cache-api.service';
export * from './cache-utils';
export * from './http-cache-interceptor';

View File

@ -1 +0,0 @@
export * from './caches/index';

View File

@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"include": ["src/**/*.ts"]
}

View File

@ -23,7 +23,6 @@
"@i18n/*": ["apps/red-ui/src/app/i18n/*"],
"@iqser/common-ui": ["libs/common-ui/src/index.ts"],
"@models/*": ["apps/red-ui/src/app/models/*"],
"@iqser/cache": ["libs/iqser-cache/src/index.ts"],
"@red/domain": ["libs/red-domain/src/index.ts"],
"@services/*": ["apps/red-ui/src/app/services/*"],
"@shared/*": ["apps/red-ui/src/app/modules/shared/*"],