diff --git a/angular.json b/angular.json index a495a40e1..c05451e7d 100644 --- a/angular.json +++ b/angular.json @@ -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", diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 46e335b8f..caeffc461 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts index 6be628d8d..a1e6d8d81 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts @@ -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 { const downloadFile$ = this.#getFileToDownload(cacheIdentifier); - const obs = wipeCaches ? from(wipeFilesCache()) : of({}); + const obs = wipeCaches ? from(wipeCache('files')) : of({}); return obs.pipe(switchMap(() => downloadFile$)); } diff --git a/apps/red-ui/src/app/services/config.service.ts b/apps/red-ui/src/app/services/config.service.ts index a2431dc25..d4b568ce6 100644 --- a/apps/red-ui/src/app/services/config.service.ts +++ b/apps/red-ui/src/app/services/config.service.ts @@ -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'; diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index 6fd6d4a96..5e53e5faa 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -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 { protected readonly _entityClass = User; readonly #currentUser$ = new BehaviorSubject(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 { } logout() { - wipeCaches().then(); + this._cacheApiService.wipeCaches().then(); this._keycloakService.logout().then(); } diff --git a/apps/red-ui/src/app/utils/constants.ts b/apps/red-ui/src/app/utils/constants.ts index 7f912541c..b5d27cb25 100644 --- a/apps/red-ui/src/app/utils/constants.ts +++ b/apps/red-ui/src/app/utils/constants.ts @@ -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'], + }, +]; diff --git a/libs/common-ui b/libs/common-ui index cd09d7254..2bc66542a 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit cd09d7254551d0f091d49a8be22c1bdfc5b8e60b +Subproject commit 2bc66542a6032b7b54cc3a3acec9737af6618031 diff --git a/libs/iqser-cache/.eslintrc.json b/libs/iqser-cache/.eslintrc.json deleted file mode 100644 index 894390ec2..000000000 --- a/libs/iqser-cache/.eslintrc.json +++ /dev/null @@ -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" - } - } - ] -} diff --git a/libs/iqser-cache/README.md b/libs/iqser-cache/README.md deleted file mode 100644 index a082dceb1..000000000 --- a/libs/iqser-cache/README.md +++ /dev/null @@ -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. diff --git a/libs/iqser-cache/src/index.ts b/libs/iqser-cache/src/index.ts deleted file mode 100644 index 9d497a49d..000000000 --- a/libs/iqser-cache/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* - * Public API Surface of communication-lib - */ - -export * from './lib/index'; diff --git a/libs/iqser-cache/src/lib/caches/cache-api.service.ts b/libs/iqser-cache/src/lib/caches/cache-api.service.ts deleted file mode 100644 index c814cc12f..000000000 --- a/libs/iqser-cache/src/lib/caches/cache-api.service.ts +++ /dev/null @@ -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, httpResponse: HttpResponse): Promise { - 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): Observable> { - 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 { - 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 { - 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) { - 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) { - // 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> { - response = response.clone(); - const contentType = response.headers.get('content-type'); - - let obs: Observable; - 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, 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; - } -} diff --git a/libs/iqser-cache/src/lib/caches/cache-utils.ts b/libs/iqser-cache/src/lib/caches/cache-utils.ts deleted file mode 100644 index 76074c0fd..000000000 --- a/libs/iqser-cache/src/lib/caches/cache-utils.ts +++ /dev/null @@ -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 }); - }); -} diff --git a/libs/iqser-cache/src/lib/caches/http-cache-interceptor.ts b/libs/iqser-cache/src/lib/caches/http-cache-interceptor.ts deleted file mode 100644 index 55b501e33..000000000 --- a/libs/iqser-cache/src/lib/caches/http-cache-interceptor.ts +++ /dev/null @@ -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, next: HttpHandler): Observable> { - // 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, next: HttpHandler): Observable> { - // 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); - } - }), - ); - } -} diff --git a/libs/iqser-cache/src/lib/caches/index.ts b/libs/iqser-cache/src/lib/caches/index.ts deleted file mode 100644 index 6ecc1e580..000000000 --- a/libs/iqser-cache/src/lib/caches/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './cache-api.service'; -export * from './cache-utils'; -export * from './http-cache-interceptor'; diff --git a/libs/iqser-cache/src/lib/index.ts b/libs/iqser-cache/src/lib/index.ts deleted file mode 100644 index bbe7a7c06..000000000 --- a/libs/iqser-cache/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './caches/index'; diff --git a/libs/iqser-cache/tsconfig.json b/libs/iqser-cache/tsconfig.json deleted file mode 100644 index 4975d8328..000000000 --- a/libs/iqser-cache/tsconfig.json +++ /dev/null @@ -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"] -} diff --git a/tsconfig.json b/tsconfig.json index 2c46b7635..baaec633d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/*"],